ragent-cli 1.11.18 → 1.11.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +3155 -490
- package/dist/sbom.json +12 -12
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "ragent-cli",
|
|
34
|
-
version: "1.11.
|
|
34
|
+
version: "1.11.21",
|
|
35
35
|
description: "CLI agent for rAgent Live \u2014 browser-first terminal control plane for AI coding agents",
|
|
36
36
|
main: "dist/index.js",
|
|
37
37
|
bin: {
|
|
@@ -90,7 +90,7 @@ var require_package = __commonJS({
|
|
|
90
90
|
commander: "^14.0.3",
|
|
91
91
|
figlet: "^1.9.3",
|
|
92
92
|
"node-pty": "^1.1.0",
|
|
93
|
-
ws: "^8.
|
|
93
|
+
ws: "^8.21.0"
|
|
94
94
|
},
|
|
95
95
|
devDependencies: {
|
|
96
96
|
"@changesets/changelog-github": "^0.5.2",
|
|
@@ -421,6 +421,7 @@ var fs2 = __toESM(require("fs"));
|
|
|
421
421
|
var path2 = __toESM(require("path"));
|
|
422
422
|
var os3 = __toESM(require("os"));
|
|
423
423
|
var import_child_process2 = require("child_process");
|
|
424
|
+
var import_string_decoder = require("string_decoder");
|
|
424
425
|
|
|
425
426
|
// src/transcript-parser-v2.ts
|
|
426
427
|
var log4 = createLogger("transcript-parser-v2");
|
|
@@ -786,6 +787,34 @@ function extractCodexText(content) {
|
|
|
786
787
|
(c) => (c.type === "input_text" || c.type === "output_text" || c.type === "text") && typeof c.text === "string"
|
|
787
788
|
).map((c) => c.text).join("\n").trim();
|
|
788
789
|
}
|
|
790
|
+
function normalizeCodexFunctionCallOutput(output) {
|
|
791
|
+
if (typeof output === "string") return output;
|
|
792
|
+
if (output == null) return "";
|
|
793
|
+
const formatPart = (part) => {
|
|
794
|
+
if (typeof part === "string") return part;
|
|
795
|
+
if (part == null) return "";
|
|
796
|
+
if (typeof part !== "object" || Array.isArray(part)) {
|
|
797
|
+
return String(part);
|
|
798
|
+
}
|
|
799
|
+
const content = part;
|
|
800
|
+
const type = typeof content.type === "string" ? content.type : "";
|
|
801
|
+
if ((type === "input_text" || type === "output_text" || type === "text") && typeof content.text === "string") {
|
|
802
|
+
return content.text;
|
|
803
|
+
}
|
|
804
|
+
if (type === "input_image" || type === "output_image" || type === "image" || typeof content.image_url === "string") {
|
|
805
|
+
return "[Image output omitted from transcript]";
|
|
806
|
+
}
|
|
807
|
+
try {
|
|
808
|
+
return JSON.stringify(part);
|
|
809
|
+
} catch {
|
|
810
|
+
return "[Unsupported tool output]";
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
if (Array.isArray(output)) {
|
|
814
|
+
return output.map(formatPart).filter((part) => part.length > 0).join("\n");
|
|
815
|
+
}
|
|
816
|
+
return formatPart(output);
|
|
817
|
+
}
|
|
789
818
|
function parseCodexArguments(args) {
|
|
790
819
|
if (typeof args !== "string" || !args) return void 0;
|
|
791
820
|
try {
|
|
@@ -1081,7 +1110,7 @@ var CodexCliParserV2 = class {
|
|
|
1081
1110
|
if (payload.type === "function_call_output") {
|
|
1082
1111
|
if (!payload.call_id) return [];
|
|
1083
1112
|
const pending = this.pendingTools.get(payload.call_id);
|
|
1084
|
-
const output = payload.output
|
|
1113
|
+
const output = normalizeCodexFunctionCallOutput(payload.output);
|
|
1085
1114
|
const { state, errorText } = inferCodexFunctionCallOutputState(output);
|
|
1086
1115
|
if (pending) {
|
|
1087
1116
|
this.pendingTools.delete(payload.call_id);
|
|
@@ -1823,37 +1852,52 @@ function createEmptySnapshot(sessionId) {
|
|
|
1823
1852
|
};
|
|
1824
1853
|
}
|
|
1825
1854
|
function applyOps(state, ops) {
|
|
1826
|
-
|
|
1827
|
-
|
|
1855
|
+
return applyReducedOps(state, coalesceAdjacentAppendOps(ops));
|
|
1856
|
+
}
|
|
1857
|
+
function applyReducedOps(state, ops) {
|
|
1858
|
+
let snapshotFields = state;
|
|
1859
|
+
let turns = { ...state.turns };
|
|
1860
|
+
let turnOrder = [...state.turnOrder];
|
|
1861
|
+
let sizeIndex = getSnapshotSizeIndex(state);
|
|
1828
1862
|
for (const op of ops) {
|
|
1863
|
+
let changedTurnId;
|
|
1864
|
+
let previousChangedTurn;
|
|
1865
|
+
let addedOrderId;
|
|
1829
1866
|
switch (op.op) {
|
|
1830
1867
|
case "upsert_turn": {
|
|
1831
1868
|
const { turn } = op;
|
|
1832
1869
|
const existing = turns[turn.turnId];
|
|
1870
|
+
previousChangedTurn = existing;
|
|
1833
1871
|
if (existing) {
|
|
1834
1872
|
turns[turn.turnId] = {
|
|
1835
1873
|
...existing,
|
|
1836
1874
|
...turn,
|
|
1837
|
-
blockOrder: turn.blockOrder
|
|
1838
|
-
blocks: turn.blocks ? { ...existing.blocks, ...turn.blocks } : existing.blocks
|
|
1875
|
+
blockOrder: turn.blockOrder ? [...turn.blockOrder] : existing.blockOrder,
|
|
1876
|
+
blocks: turn.blocks ? { ...existing.blocks, ...cloneBlocks(turn.blocks) } : existing.blocks
|
|
1839
1877
|
};
|
|
1840
1878
|
} else {
|
|
1879
|
+
if (snapshotFields.truncatedTurnIds?.includes(turn.turnId)) break;
|
|
1841
1880
|
turns[turn.turnId] = {
|
|
1842
1881
|
...turn,
|
|
1843
|
-
blockOrder: turn.blockOrder
|
|
1844
|
-
blocks: turn.blocks
|
|
1882
|
+
blockOrder: turn.blockOrder ? [...turn.blockOrder] : [],
|
|
1883
|
+
blocks: turn.blocks ? cloneBlocks(turn.blocks) : {}
|
|
1845
1884
|
};
|
|
1846
1885
|
if (!turnOrder.includes(turn.turnId)) {
|
|
1847
1886
|
turnOrder.push(turn.turnId);
|
|
1887
|
+
addedOrderId = turn.turnId;
|
|
1848
1888
|
}
|
|
1849
1889
|
}
|
|
1890
|
+
changedTurnId = turn.turnId;
|
|
1850
1891
|
break;
|
|
1851
1892
|
}
|
|
1852
1893
|
case "insert_block": {
|
|
1853
1894
|
const turn = turns[op.turnId];
|
|
1854
1895
|
if (!turn) break;
|
|
1855
|
-
|
|
1856
|
-
const
|
|
1896
|
+
previousChangedTurn = turn;
|
|
1897
|
+
const newBlocks = { ...turn.blocks, [op.block.blockId]: { ...op.block } };
|
|
1898
|
+
const newBlockOrder = turn.blockOrder.filter(
|
|
1899
|
+
(blockId) => blockId !== op.block.blockId
|
|
1900
|
+
);
|
|
1857
1901
|
if (op.afterBlockId) {
|
|
1858
1902
|
const idx = newBlockOrder.indexOf(op.afterBlockId);
|
|
1859
1903
|
if (idx >= 0) {
|
|
@@ -1870,28 +1914,33 @@ function applyOps(state, ops) {
|
|
|
1870
1914
|
blockOrder: newBlockOrder,
|
|
1871
1915
|
updatedAt: op.block.updatedAt
|
|
1872
1916
|
};
|
|
1917
|
+
changedTurnId = op.turnId;
|
|
1873
1918
|
break;
|
|
1874
1919
|
}
|
|
1875
1920
|
case "replace_block": {
|
|
1876
1921
|
const turn = turns[op.turnId];
|
|
1877
1922
|
if (!turn || !turn.blocks[op.block.blockId]) break;
|
|
1923
|
+
previousChangedTurn = turn;
|
|
1878
1924
|
turns[op.turnId] = {
|
|
1879
1925
|
...turn,
|
|
1880
|
-
blocks: { ...turn.blocks, [op.block.blockId]: op.block },
|
|
1926
|
+
blocks: { ...turn.blocks, [op.block.blockId]: { ...op.block } },
|
|
1881
1927
|
updatedAt: op.block.updatedAt
|
|
1882
1928
|
};
|
|
1929
|
+
changedTurnId = op.turnId;
|
|
1883
1930
|
break;
|
|
1884
1931
|
}
|
|
1885
1932
|
case "append_text": {
|
|
1886
1933
|
const block = findBlock(turns, op.turnId, op.blockId);
|
|
1887
1934
|
if (!block) break;
|
|
1888
1935
|
if (block.kind === "text" || block.kind === "reasoning") {
|
|
1936
|
+
previousChangedTurn = turns[op.turnId];
|
|
1889
1937
|
const updated = {
|
|
1890
1938
|
...block,
|
|
1891
|
-
text: block.text
|
|
1892
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1939
|
+
text: appendProjectedSnapshotText(block, block.text, op.text)
|
|
1893
1940
|
};
|
|
1941
|
+
refreshTransportTruncation(updated, op.text);
|
|
1894
1942
|
setBlock(turns, op.turnId, op.blockId, updated);
|
|
1943
|
+
changedTurnId = op.turnId;
|
|
1895
1944
|
}
|
|
1896
1945
|
break;
|
|
1897
1946
|
}
|
|
@@ -1900,21 +1949,33 @@ function applyOps(state, ops) {
|
|
|
1900
1949
|
if (!block || block.kind !== "tool") {
|
|
1901
1950
|
const found = findBlockAcrossTurns(turns, op.blockId);
|
|
1902
1951
|
if (found && found.block.kind === "tool") {
|
|
1952
|
+
previousChangedTurn = turns[found.turnId];
|
|
1903
1953
|
const updated2 = {
|
|
1904
1954
|
...found.block,
|
|
1905
|
-
outputText: (
|
|
1906
|
-
|
|
1955
|
+
outputText: appendProjectedSnapshotText(
|
|
1956
|
+
found.block,
|
|
1957
|
+
found.block.outputText ?? "",
|
|
1958
|
+
op.text
|
|
1959
|
+
)
|
|
1907
1960
|
};
|
|
1961
|
+
refreshTransportTruncation(updated2, op.text);
|
|
1908
1962
|
setBlock(turns, found.turnId, op.blockId, updated2);
|
|
1963
|
+
changedTurnId = found.turnId;
|
|
1909
1964
|
}
|
|
1910
1965
|
break;
|
|
1911
1966
|
}
|
|
1912
1967
|
const updated = {
|
|
1913
1968
|
...block,
|
|
1914
|
-
outputText: (
|
|
1915
|
-
|
|
1969
|
+
outputText: appendProjectedSnapshotText(
|
|
1970
|
+
block,
|
|
1971
|
+
block.outputText ?? "",
|
|
1972
|
+
op.text
|
|
1973
|
+
)
|
|
1916
1974
|
};
|
|
1975
|
+
previousChangedTurn = turns[op.turnId];
|
|
1976
|
+
refreshTransportTruncation(updated, op.text);
|
|
1917
1977
|
setBlock(turns, op.turnId, op.blockId, updated);
|
|
1978
|
+
changedTurnId = op.turnId;
|
|
1918
1979
|
break;
|
|
1919
1980
|
}
|
|
1920
1981
|
case "set_tool_state": {
|
|
@@ -1934,10 +1995,11 @@ function applyOps(state, ops) {
|
|
|
1934
1995
|
state: op.state,
|
|
1935
1996
|
...op.exitCode !== void 0 && { exitCode: op.exitCode },
|
|
1936
1997
|
...op.errorText !== void 0 && { errorText: op.errorText },
|
|
1937
|
-
...op.isComplete !== void 0 && { isComplete: op.isComplete }
|
|
1938
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1998
|
+
...op.isComplete !== void 0 && { isComplete: op.isComplete }
|
|
1939
1999
|
};
|
|
2000
|
+
previousChangedTurn = turns[resolvedTurnId];
|
|
1940
2001
|
setBlock(turns, resolvedTurnId, op.blockId, updated);
|
|
2002
|
+
changedTurnId = resolvedTurnId;
|
|
1941
2003
|
break;
|
|
1942
2004
|
}
|
|
1943
2005
|
case "complete_block": {
|
|
@@ -1957,27 +2019,93 @@ function applyOps(state, ops) {
|
|
|
1957
2019
|
isComplete: true,
|
|
1958
2020
|
updatedAt: op.updatedAt
|
|
1959
2021
|
};
|
|
2022
|
+
previousChangedTurn = turns[resolvedTurnId];
|
|
1960
2023
|
setBlock(turns, resolvedTurnId, op.blockId, updated);
|
|
2024
|
+
changedTurnId = resolvedTurnId;
|
|
1961
2025
|
break;
|
|
1962
2026
|
}
|
|
1963
2027
|
case "complete_turn": {
|
|
1964
2028
|
const turn = turns[op.turnId];
|
|
1965
2029
|
if (!turn) break;
|
|
2030
|
+
previousChangedTurn = turn;
|
|
1966
2031
|
turns[op.turnId] = {
|
|
1967
2032
|
...turn,
|
|
1968
2033
|
isComplete: true,
|
|
1969
2034
|
endedAt: op.endedAt,
|
|
1970
2035
|
updatedAt: op.updatedAt
|
|
1971
2036
|
};
|
|
2037
|
+
changedTurnId = op.turnId;
|
|
1972
2038
|
break;
|
|
1973
2039
|
}
|
|
1974
2040
|
}
|
|
2041
|
+
if (changedTurnId) {
|
|
2042
|
+
updateIndexedTurn(
|
|
2043
|
+
sizeIndex,
|
|
2044
|
+
changedTurnId,
|
|
2045
|
+
previousChangedTurn,
|
|
2046
|
+
turns[changedTurnId]
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
2049
|
+
if (addedOrderId) addIndexedTurnOrder(sizeIndex, addedOrderId);
|
|
2050
|
+
const projected = projectWorkingSnapshot(
|
|
2051
|
+
snapshotFields,
|
|
2052
|
+
turnOrder,
|
|
2053
|
+
turns,
|
|
2054
|
+
sizeIndex
|
|
2055
|
+
);
|
|
2056
|
+
snapshotFields = projected.snapshotFields;
|
|
2057
|
+
turnOrder = projected.turnOrder;
|
|
2058
|
+
turns = projected.turns;
|
|
2059
|
+
sizeIndex = projected.sizeIndex;
|
|
1975
2060
|
}
|
|
1976
|
-
|
|
1977
|
-
...
|
|
2061
|
+
const result = {
|
|
2062
|
+
...snapshotFields,
|
|
1978
2063
|
turnOrder,
|
|
1979
2064
|
turns
|
|
1980
2065
|
};
|
|
2066
|
+
cacheSnapshotSizeIndex(result, sizeIndex);
|
|
2067
|
+
return result;
|
|
2068
|
+
}
|
|
2069
|
+
function cloneBlocks(blocks) {
|
|
2070
|
+
const cloned = {};
|
|
2071
|
+
for (const [blockId, block] of Object.entries(blocks)) {
|
|
2072
|
+
cloned[blockId] = { ...block };
|
|
2073
|
+
}
|
|
2074
|
+
return cloned;
|
|
2075
|
+
}
|
|
2076
|
+
function coalesceAdjacentAppendOps(ops) {
|
|
2077
|
+
const result = [];
|
|
2078
|
+
let pending;
|
|
2079
|
+
const flush = () => {
|
|
2080
|
+
if (!pending) return;
|
|
2081
|
+
result.push({
|
|
2082
|
+
op: pending.op,
|
|
2083
|
+
turnId: pending.turnId,
|
|
2084
|
+
blockId: pending.blockId,
|
|
2085
|
+
text: pending.chunks.join("")
|
|
2086
|
+
});
|
|
2087
|
+
pending = void 0;
|
|
2088
|
+
};
|
|
2089
|
+
for (const op of ops) {
|
|
2090
|
+
if (op.op === "append_text" || op.op === "append_tool_output") {
|
|
2091
|
+
if (pending?.op === op.op && pending.turnId === op.turnId && pending.blockId === op.blockId) {
|
|
2092
|
+
pending.chunks.push(op.text);
|
|
2093
|
+
} else {
|
|
2094
|
+
flush();
|
|
2095
|
+
pending = {
|
|
2096
|
+
op: op.op,
|
|
2097
|
+
turnId: op.turnId,
|
|
2098
|
+
blockId: op.blockId,
|
|
2099
|
+
chunks: [op.text]
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
continue;
|
|
2103
|
+
}
|
|
2104
|
+
flush();
|
|
2105
|
+
result.push(op);
|
|
2106
|
+
}
|
|
2107
|
+
flush();
|
|
2108
|
+
return result;
|
|
1981
2109
|
}
|
|
1982
2110
|
function findBlock(turns, turnId, blockId) {
|
|
1983
2111
|
return turns[turnId]?.blocks[blockId];
|
|
@@ -1999,50 +2127,944 @@ function setBlock(turns, turnId, blockId, block) {
|
|
|
1999
2127
|
};
|
|
2000
2128
|
}
|
|
2001
2129
|
var MAX_SNAPSHOT_TURNS = 1e3;
|
|
2002
|
-
var
|
|
2003
|
-
var
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2130
|
+
var MAX_SNAPSHOT_SIZE_BYTES = 448 * 1024;
|
|
2131
|
+
var MAX_TRUNCATED_TURN_ID_BYTES = 16 * 1024;
|
|
2132
|
+
var SNAPSHOT_FIELD_RESERVE = MAX_TRUNCATED_TURN_ID_BYTES + 512;
|
|
2133
|
+
var MAX_SNAPSHOT_BLOCK_FIELD_BYTES = 96 * 1024;
|
|
2134
|
+
var MAX_SNAPSHOT_IDENTIFIER_BYTES = 4 * 1024;
|
|
2135
|
+
var MAX_SNAPSHOT_TIMESTAMP_BYTES = 1024;
|
|
2136
|
+
var SNAPSHOT_TRUNCATION_MARKER = "\n\n[Content truncated for remote display]\n\n";
|
|
2137
|
+
var TRANSPORT_NOTICE_METADATA_KEY = "ragentTransportTruncationNotice";
|
|
2138
|
+
var TRANSPORT_OMITTED_BLOCKS_KEY = "ragentTransportOmittedBlocks";
|
|
2139
|
+
var utf8Encoder = new TextEncoder();
|
|
2140
|
+
function utf8Size(value) {
|
|
2141
|
+
return utf8Encoder.encode(value).byteLength;
|
|
2142
|
+
}
|
|
2143
|
+
function serializedBytes(value) {
|
|
2144
|
+
return utf8Size(JSON.stringify(value));
|
|
2145
|
+
}
|
|
2146
|
+
var snapshotSizeCache = /* @__PURE__ */ new WeakMap();
|
|
2147
|
+
function cloneSnapshotSizeIndex(index) {
|
|
2148
|
+
return { ...index };
|
|
2149
|
+
}
|
|
2150
|
+
function snapshotSizeFromIndex(index) {
|
|
2151
|
+
return index.shellBytes + index.orderBytes + index.turnMapBytes;
|
|
2152
|
+
}
|
|
2153
|
+
function buildSnapshotSizeIndex(snapshot) {
|
|
2154
|
+
const shellBytes = serializedBytes({
|
|
2155
|
+
...snapshot,
|
|
2156
|
+
turnOrder: [],
|
|
2157
|
+
turns: {}
|
|
2158
|
+
});
|
|
2159
|
+
let orderBytes = 0;
|
|
2160
|
+
for (let index = 0; index < snapshot.turnOrder.length; index++) {
|
|
2161
|
+
orderBytes += utf8Size(JSON.stringify(snapshot.turnOrder[index])) + (index > 0 ? 1 : 0);
|
|
2162
|
+
}
|
|
2163
|
+
let turnMapBytes = 0;
|
|
2164
|
+
let turnIndex = 0;
|
|
2165
|
+
for (const [turnId, turn] of Object.entries(snapshot.turns)) {
|
|
2166
|
+
const bytes = serializedBytes(turn);
|
|
2167
|
+
turnMapBytes += utf8Size(JSON.stringify(turnId)) + 1 + bytes + (turnIndex > 0 ? 1 : 0);
|
|
2168
|
+
turnIndex++;
|
|
2169
|
+
}
|
|
2170
|
+
return {
|
|
2171
|
+
shellBytes,
|
|
2172
|
+
orderBytes,
|
|
2173
|
+
orderCount: snapshot.turnOrder.length,
|
|
2174
|
+
turnMapBytes,
|
|
2175
|
+
turnCount: turnIndex
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
function getSnapshotSizeIndex(snapshot) {
|
|
2179
|
+
const cached = snapshotSizeCache.get(snapshot);
|
|
2180
|
+
return cached ? cloneSnapshotSizeIndex(cached) : buildSnapshotSizeIndex(snapshot);
|
|
2181
|
+
}
|
|
2182
|
+
function cacheSnapshotSizeIndex(snapshot, index) {
|
|
2183
|
+
snapshotSizeCache.set(snapshot, cloneSnapshotSizeIndex(index));
|
|
2184
|
+
}
|
|
2185
|
+
function updateIndexedTurn(index, turnId, previousTurn, nextTurn) {
|
|
2186
|
+
const previousBytes = previousTurn ? serializedBytes(previousTurn) : void 0;
|
|
2187
|
+
if (!nextTurn) {
|
|
2188
|
+
if (previousBytes === void 0) return;
|
|
2189
|
+
index.turnMapBytes -= utf8Size(JSON.stringify(turnId)) + 1 + previousBytes + (index.turnCount > 1 ? 1 : 0);
|
|
2190
|
+
index.turnCount--;
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
const nextBytes = serializedBytes(nextTurn);
|
|
2194
|
+
if (previousBytes === void 0) {
|
|
2195
|
+
index.turnMapBytes += utf8Size(JSON.stringify(turnId)) + 1 + nextBytes + (index.turnCount > 0 ? 1 : 0);
|
|
2196
|
+
index.turnCount++;
|
|
2197
|
+
} else {
|
|
2198
|
+
index.turnMapBytes += nextBytes - previousBytes;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
function addIndexedTurnOrder(index, turnId) {
|
|
2202
|
+
index.orderBytes += utf8Size(JSON.stringify(turnId)) + (index.orderCount > 0 ? 1 : 0);
|
|
2203
|
+
index.orderCount++;
|
|
2204
|
+
}
|
|
2205
|
+
function removeIndexedTurnOrder(index, turnId) {
|
|
2206
|
+
if (index.orderCount <= 0) return;
|
|
2207
|
+
index.orderBytes -= utf8Size(JSON.stringify(turnId)) + (index.orderCount > 1 ? 1 : 0);
|
|
2208
|
+
index.orderCount--;
|
|
2209
|
+
}
|
|
2210
|
+
function refreshIndexedShell(index, snapshotFields) {
|
|
2211
|
+
index.shellBytes = serializedBytes({
|
|
2212
|
+
...snapshotFields,
|
|
2213
|
+
turnOrder: [],
|
|
2214
|
+
turns: {}
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2217
|
+
function projectWorkingSnapshot(initialFields, turnOrder, turns, sizeIndex) {
|
|
2218
|
+
let snapshotFields = initialFields;
|
|
2219
|
+
const sizeBudget = MAX_SNAPSHOT_SIZE_BYTES - SNAPSHOT_FIELD_RESERVE;
|
|
2220
|
+
while ((turnOrder.length > MAX_SNAPSHOT_TURNS || snapshotSizeFromIndex(sizeIndex) > sizeBudget) && turnOrder.length > 1) {
|
|
2221
|
+
const dropped = [];
|
|
2222
|
+
while ((turnOrder.length > MAX_SNAPSHOT_TURNS || snapshotSizeFromIndex(sizeIndex) > sizeBudget) && turnOrder.length > 1) {
|
|
2223
|
+
const turnId = turnOrder.shift();
|
|
2224
|
+
if (!turnId) break;
|
|
2225
|
+
removeIndexedTurnOrder(sizeIndex, turnId);
|
|
2226
|
+
if (!turnOrder.includes(turnId)) {
|
|
2227
|
+
updateIndexedTurn(sizeIndex, turnId, turns[turnId], void 0);
|
|
2228
|
+
delete turns[turnId];
|
|
2229
|
+
}
|
|
2230
|
+
dropped.push(turnId);
|
|
2231
|
+
}
|
|
2232
|
+
if (dropped.length === 0) break;
|
|
2233
|
+
snapshotFields = {
|
|
2234
|
+
...snapshotFields,
|
|
2235
|
+
truncatedTurns: (snapshotFields.truncatedTurns ?? 0) + dropped.length,
|
|
2236
|
+
truncatedTurnIds: mergeTruncatedTurnIds(
|
|
2237
|
+
snapshotFields.truncatedTurnIds,
|
|
2238
|
+
dropped
|
|
2239
|
+
)
|
|
2240
|
+
};
|
|
2241
|
+
refreshIndexedShell(sizeIndex, snapshotFields);
|
|
2242
|
+
}
|
|
2243
|
+
const soleTurnId = turnOrder.length === 1 ? turnOrder[0] : void 0;
|
|
2244
|
+
const soleTurn = soleTurnId ? turns[soleTurnId] : void 0;
|
|
2245
|
+
const omittedBlocks = soleTurn?.metadata?.[TRANSPORT_OMITTED_BLOCKS_KEY];
|
|
2246
|
+
const missingSyntheticNotice = typeof omittedBlocks === "number" && omittedBlocks > 0 && !soleTurn?.blockOrder.some((blockId) => soleTurn.blocks[blockId]?.metadata?.[TRANSPORT_NOTICE_METADATA_KEY] === true);
|
|
2247
|
+
if (soleTurn && (snapshotSizeFromIndex(sizeIndex) > sizeBudget || missingSyntheticNotice)) {
|
|
2248
|
+
let bounded = boundSingleTurnSnapshot({
|
|
2249
|
+
...snapshotFields,
|
|
2250
|
+
turnOrder,
|
|
2251
|
+
turns
|
|
2252
|
+
});
|
|
2253
|
+
let boundedIndex = buildSnapshotSizeIndex(bounded);
|
|
2254
|
+
if (snapshotSizeFromIndex(boundedIndex) > MAX_SNAPSHOT_SIZE_BYTES) {
|
|
2255
|
+
bounded = minimalTransportSnapshot(bounded, bounded.truncatedTurns);
|
|
2256
|
+
boundedIndex = buildSnapshotSizeIndex(bounded);
|
|
2257
|
+
}
|
|
2258
|
+
return {
|
|
2259
|
+
snapshotFields: bounded,
|
|
2260
|
+
turnOrder: [...bounded.turnOrder],
|
|
2261
|
+
turns: { ...bounded.turns },
|
|
2262
|
+
sizeIndex: boundedIndex
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
return { snapshotFields, turnOrder, turns, sizeIndex };
|
|
2266
|
+
}
|
|
2267
|
+
function prefixWithinBytes(value, maxBytes) {
|
|
2268
|
+
let low = 0;
|
|
2269
|
+
let high = value.length;
|
|
2270
|
+
while (low < high) {
|
|
2271
|
+
const mid = Math.ceil((low + high) / 2);
|
|
2272
|
+
if (utf8Size(value.slice(0, mid)) <= maxBytes) low = mid;
|
|
2273
|
+
else high = mid - 1;
|
|
2274
|
+
}
|
|
2275
|
+
if (low > 0 && /[\uD800-\uDBFF]/.test(value[low - 1] ?? "")) low--;
|
|
2276
|
+
return value.slice(0, low);
|
|
2277
|
+
}
|
|
2278
|
+
function suffixWithinBytes(value, maxBytes) {
|
|
2279
|
+
let low = 0;
|
|
2280
|
+
let high = value.length;
|
|
2281
|
+
while (low < high) {
|
|
2282
|
+
const count = Math.ceil((low + high) / 2);
|
|
2283
|
+
if (utf8Size(value.slice(value.length - count)) <= maxBytes) low = count;
|
|
2284
|
+
else high = count - 1;
|
|
2285
|
+
}
|
|
2286
|
+
let start = value.length - low;
|
|
2287
|
+
if (start < value.length && /[\uDC00-\uDFFF]/.test(value[start] ?? "")) start++;
|
|
2288
|
+
return value.slice(start);
|
|
2289
|
+
}
|
|
2290
|
+
function truncateSnapshotText(value, maxBytes) {
|
|
2291
|
+
if (utf8Size(value) <= maxBytes) return value;
|
|
2292
|
+
const markerBytes = utf8Size(SNAPSHOT_TRUNCATION_MARKER);
|
|
2293
|
+
if (maxBytes <= markerBytes) {
|
|
2294
|
+
return prefixWithinBytes(SNAPSHOT_TRUNCATION_MARKER, maxBytes);
|
|
2295
|
+
}
|
|
2296
|
+
const available = maxBytes - markerBytes;
|
|
2297
|
+
const markerIndex = value.indexOf(SNAPSHOT_TRUNCATION_MARKER);
|
|
2298
|
+
const headSource = markerIndex >= 0 ? value.slice(0, markerIndex) : value;
|
|
2299
|
+
const tailSource = markerIndex >= 0 ? value.slice(markerIndex + SNAPSHOT_TRUNCATION_MARKER.length) : value;
|
|
2300
|
+
const head = prefixWithinBytes(headSource, Math.floor(available * 0.75));
|
|
2301
|
+
const tail = suffixWithinBytes(tailSource, available - utf8Size(head));
|
|
2302
|
+
return `${head}${SNAPSHOT_TRUNCATION_MARKER}${tail}`;
|
|
2303
|
+
}
|
|
2304
|
+
function appendProjectedSnapshotText(block, current, addition) {
|
|
2305
|
+
if (block.truncation?.reason === "transport_limit" && current.includes(SNAPSHOT_TRUNCATION_MARKER)) {
|
|
2306
|
+
const markerIndex = current.indexOf(SNAPSHOT_TRUNCATION_MARKER);
|
|
2307
|
+
const head = current.slice(0, markerIndex);
|
|
2308
|
+
const tail = current.slice(markerIndex + SNAPSHOT_TRUNCATION_MARKER.length);
|
|
2309
|
+
const tailBudget = Math.max(
|
|
2310
|
+
0,
|
|
2311
|
+
utf8Size(current) - utf8Size(head) - utf8Size(SNAPSHOT_TRUNCATION_MARKER)
|
|
2312
|
+
);
|
|
2313
|
+
return `${head}${SNAPSHOT_TRUNCATION_MARKER}${suffixWithinBytes(
|
|
2314
|
+
tail + addition,
|
|
2315
|
+
tailBudget
|
|
2316
|
+
)}`;
|
|
2317
|
+
}
|
|
2318
|
+
return current + addition;
|
|
2319
|
+
}
|
|
2320
|
+
function refreshTransportTruncation(block, _addition) {
|
|
2321
|
+
const projectedText = block.kind === "text" || block.kind === "reasoning" ? block.text : block.kind === "tool" ? block.outputText : void 0;
|
|
2322
|
+
if (block.truncation?.reason !== "transport_limit" || !projectedText?.includes(SNAPSHOT_TRUNCATION_MARKER)) {
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
const { truncation: _truncation, ...withoutTruncation } = block;
|
|
2326
|
+
block.truncation = {
|
|
2327
|
+
reason: "transport_limit",
|
|
2328
|
+
keptBytes: serializedBytes(withoutTruncation),
|
|
2329
|
+
didWarnUser: true
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
function boundSnapshotBlock(block, fieldBudget = MAX_SNAPSHOT_BLOCK_FIELD_BYTES) {
|
|
2333
|
+
const { truncation: existingTruncation, ...originalWithoutTruncation } = block;
|
|
2334
|
+
const originalBytes = serializedBytes(originalWithoutTruncation);
|
|
2335
|
+
let bounded;
|
|
2336
|
+
switch (block.kind) {
|
|
2337
|
+
case "text":
|
|
2338
|
+
case "reasoning":
|
|
2339
|
+
bounded = { ...block, metadata: void 0, text: truncateSnapshotText(block.text, fieldBudget) };
|
|
2340
|
+
break;
|
|
2341
|
+
case "tool": {
|
|
2342
|
+
let input = block.input;
|
|
2343
|
+
if (input !== void 0 && serializedBytes(input) > 16 * 1024) {
|
|
2344
|
+
input = "[Tool input truncated for remote display]";
|
|
2345
|
+
}
|
|
2346
|
+
bounded = {
|
|
2347
|
+
...block,
|
|
2348
|
+
metadata: void 0,
|
|
2349
|
+
callId: truncateSnapshotText(block.callId, MAX_SNAPSHOT_IDENTIFIER_BYTES),
|
|
2350
|
+
toolName: truncateSnapshotText(block.toolName, 2 * 1024),
|
|
2351
|
+
input,
|
|
2352
|
+
outputText: block.outputText === void 0 ? void 0 : truncateSnapshotText(block.outputText, fieldBudget),
|
|
2353
|
+
errorText: block.errorText === void 0 ? void 0 : truncateSnapshotText(block.errorText, 32 * 1024),
|
|
2354
|
+
outputMimeType: block.outputMimeType === void 0 ? void 0 : truncateSnapshotText(block.outputMimeType, 2 * 1024)
|
|
2355
|
+
};
|
|
2356
|
+
break;
|
|
2357
|
+
}
|
|
2358
|
+
case "status":
|
|
2359
|
+
bounded = {
|
|
2360
|
+
...block,
|
|
2361
|
+
metadata: void 0,
|
|
2362
|
+
statusType: truncateSnapshotText(block.statusType, 2 * 1024),
|
|
2363
|
+
text: block.text === void 0 ? void 0 : truncateSnapshotText(block.text, 32 * 1024)
|
|
2364
|
+
};
|
|
2365
|
+
break;
|
|
2366
|
+
case "unknown":
|
|
2367
|
+
bounded = {
|
|
2368
|
+
...block,
|
|
2369
|
+
metadata: void 0,
|
|
2370
|
+
rawKind: truncateSnapshotText(block.rawKind, 2 * 1024),
|
|
2371
|
+
rawPayload: block.rawPayload !== void 0 && serializedBytes(block.rawPayload) > 16 * 1024 ? "[Unknown payload truncated for remote display]" : block.rawPayload,
|
|
2372
|
+
fallbackText: block.fallbackText === void 0 ? void 0 : truncateSnapshotText(block.fallbackText, 32 * 1024)
|
|
2373
|
+
};
|
|
2374
|
+
break;
|
|
2375
|
+
case "attachment":
|
|
2376
|
+
bounded = {
|
|
2377
|
+
...block,
|
|
2378
|
+
metadata: void 0,
|
|
2379
|
+
mediaType: truncateSnapshotText(block.mediaType, 2 * 1024),
|
|
2380
|
+
name: block.name === void 0 ? void 0 : truncateSnapshotText(block.name, 2 * 1024),
|
|
2381
|
+
uri: block.uri === void 0 ? void 0 : truncateSnapshotText(block.uri, 8 * 1024),
|
|
2382
|
+
path: block.path === void 0 ? void 0 : truncateSnapshotText(block.path, 8 * 1024)
|
|
2383
|
+
};
|
|
2384
|
+
break;
|
|
2385
|
+
}
|
|
2386
|
+
bounded = {
|
|
2387
|
+
...bounded,
|
|
2388
|
+
blockId: truncateSnapshotText(block.blockId, MAX_SNAPSHOT_IDENTIFIER_BYTES),
|
|
2389
|
+
createdAt: truncateSnapshotText(block.createdAt, MAX_SNAPSHOT_TIMESTAMP_BYTES),
|
|
2390
|
+
updatedAt: truncateSnapshotText(block.updatedAt, MAX_SNAPSHOT_TIMESTAMP_BYTES),
|
|
2391
|
+
metadata: void 0
|
|
2392
|
+
};
|
|
2393
|
+
const { truncation: _boundedTruncation, ...boundedWithoutTruncation } = bounded;
|
|
2394
|
+
const keptBytes = serializedBytes(boundedWithoutTruncation);
|
|
2395
|
+
if (keptBytes < originalBytes || existingTruncation?.reason === "transport_limit") {
|
|
2396
|
+
bounded = {
|
|
2397
|
+
...boundedWithoutTruncation,
|
|
2398
|
+
truncation: {
|
|
2399
|
+
reason: "transport_limit",
|
|
2400
|
+
keptBytes,
|
|
2401
|
+
didWarnUser: true
|
|
2402
|
+
}
|
|
2403
|
+
};
|
|
2404
|
+
} else if (existingTruncation) {
|
|
2405
|
+
bounded = { ...boundedWithoutTruncation, truncation: existingTruncation };
|
|
2406
|
+
} else {
|
|
2407
|
+
bounded = boundedWithoutTruncation;
|
|
2408
|
+
}
|
|
2409
|
+
return bounded;
|
|
2410
|
+
}
|
|
2411
|
+
function boundSingleTurnSnapshot(snapshot) {
|
|
2412
|
+
const originalTurnId = snapshot.turnOrder[0];
|
|
2413
|
+
const originalTurn = originalTurnId ? snapshot.turns[originalTurnId] : void 0;
|
|
2414
|
+
if (!originalTurnId || !originalTurn) return snapshot;
|
|
2415
|
+
const turnId = truncateSnapshotText(originalTurnId, MAX_SNAPSHOT_IDENTIFIER_BYTES);
|
|
2416
|
+
const boundedEntries = [];
|
|
2417
|
+
const usedBlockIds = /* @__PURE__ */ new Set();
|
|
2418
|
+
const storedDroppedBlocks = originalTurn.metadata?.[TRANSPORT_OMITTED_BLOCKS_KEY];
|
|
2419
|
+
let previouslyDroppedBlocks = typeof storedDroppedBlocks === "number" ? storedDroppedBlocks : 0;
|
|
2420
|
+
let collisionDrops = 0;
|
|
2421
|
+
const sourceOrder = originalTurn.blockOrder.length > 0 ? originalTurn.blockOrder : Object.keys(originalTurn.blocks);
|
|
2422
|
+
for (const originalBlockId of sourceOrder) {
|
|
2423
|
+
const block = originalTurn.blocks[originalBlockId];
|
|
2424
|
+
if (!block) continue;
|
|
2425
|
+
if (block.kind === "status" && block.metadata?.[TRANSPORT_NOTICE_METADATA_KEY] === true) {
|
|
2426
|
+
const metadataCount = block.metadata.omittedBlocks;
|
|
2427
|
+
const priorCount = typeof metadataCount === "number" ? metadataCount : Number.parseInt(block.text?.match(/(\d+) earlier blocks omitted/)?.[1] ?? "0", 10);
|
|
2428
|
+
previouslyDroppedBlocks = Math.max(
|
|
2429
|
+
previouslyDroppedBlocks,
|
|
2430
|
+
Number.isFinite(priorCount) ? priorCount : 0
|
|
2431
|
+
);
|
|
2432
|
+
continue;
|
|
2433
|
+
}
|
|
2434
|
+
const bounded = boundSnapshotBlock(block);
|
|
2435
|
+
if (usedBlockIds.has(bounded.blockId)) {
|
|
2436
|
+
collisionDrops++;
|
|
2437
|
+
continue;
|
|
2438
|
+
}
|
|
2439
|
+
usedBlockIds.add(bounded.blockId);
|
|
2440
|
+
boundedEntries.push({ id: bounded.blockId, block: bounded });
|
|
2441
|
+
}
|
|
2442
|
+
const boundedTurn = {
|
|
2443
|
+
...originalTurn,
|
|
2444
|
+
turnId,
|
|
2445
|
+
startedAt: truncateSnapshotText(originalTurn.startedAt, MAX_SNAPSHOT_TIMESTAMP_BYTES),
|
|
2446
|
+
updatedAt: truncateSnapshotText(originalTurn.updatedAt, MAX_SNAPSHOT_TIMESTAMP_BYTES),
|
|
2447
|
+
endedAt: originalTurn.endedAt === void 0 ? void 0 : truncateSnapshotText(originalTurn.endedAt, MAX_SNAPSHOT_TIMESTAMP_BYTES),
|
|
2448
|
+
agent: originalTurn.agent === void 0 ? void 0 : {
|
|
2449
|
+
vendor: originalTurn.agent.vendor === void 0 ? void 0 : truncateSnapshotText(originalTurn.agent.vendor, 2 * 1024),
|
|
2450
|
+
agentType: originalTurn.agent.agentType === void 0 ? void 0 : truncateSnapshotText(originalTurn.agent.agentType, 2 * 1024),
|
|
2451
|
+
name: originalTurn.agent.name === void 0 ? void 0 : truncateSnapshotText(originalTurn.agent.name, 2 * 1024),
|
|
2452
|
+
version: originalTurn.agent.version === void 0 ? void 0 : truncateSnapshotText(originalTurn.agent.version, 2 * 1024),
|
|
2453
|
+
transcriptSource: originalTurn.agent.transcriptSource
|
|
2454
|
+
},
|
|
2455
|
+
metadata: void 0,
|
|
2456
|
+
blockOrder: [],
|
|
2457
|
+
blocks: {}
|
|
2458
|
+
};
|
|
2459
|
+
const build = (entries, notice2, omittedBlocks = previouslyDroppedBlocks + collisionDrops) => {
|
|
2460
|
+
const blockOrder = notice2 ? [notice2.blockId, ...entries.map((entry) => entry.id)] : entries.map((entry) => entry.id);
|
|
2461
|
+
const blocks = {};
|
|
2462
|
+
if (notice2) blocks[notice2.blockId] = notice2;
|
|
2463
|
+
for (const entry of entries) blocks[entry.id] = entry.block;
|
|
2464
|
+
return {
|
|
2465
|
+
...snapshot,
|
|
2466
|
+
turnOrder: [turnId],
|
|
2467
|
+
turns: {
|
|
2468
|
+
[turnId]: {
|
|
2469
|
+
...boundedTurn,
|
|
2470
|
+
metadata: omittedBlocks > 0 ? { [TRANSPORT_OMITTED_BLOCKS_KEY]: omittedBlocks } : void 0,
|
|
2471
|
+
blockOrder,
|
|
2472
|
+
blocks
|
|
2473
|
+
}
|
|
2013
2474
|
}
|
|
2475
|
+
};
|
|
2476
|
+
};
|
|
2477
|
+
const budget = MAX_SNAPSHOT_SIZE_BYTES - SNAPSHOT_FIELD_RESERVE;
|
|
2478
|
+
const emptyResult = build([]);
|
|
2479
|
+
const emptyBytes = serializedBytes(emptyResult);
|
|
2480
|
+
let allEntriesBytes = emptyBytes;
|
|
2481
|
+
for (let index = 0; index < boundedEntries.length; index++) {
|
|
2482
|
+
const entry = boundedEntries[index];
|
|
2483
|
+
allEntriesBytes += utf8Size(JSON.stringify(entry.id)) + utf8Size(JSON.stringify(entry.id)) + 1 + serializedBytes(entry.block) + (index > 0 ? 2 : 0);
|
|
2484
|
+
}
|
|
2485
|
+
const hasVisibleTruncation = boundedEntries.some(({ block }) => block.truncation?.didWarnUser === true || "text" in block && typeof block.text === "string" && block.text.includes("[Content truncated for remote display]"));
|
|
2486
|
+
const sizeRequiresDrops = allEntriesBytes > budget;
|
|
2487
|
+
const knownDrops = previouslyDroppedBlocks + collisionDrops;
|
|
2488
|
+
const needsNotice = knownDrops > 0 || sizeRequiresDrops || !hasVisibleTruncation;
|
|
2489
|
+
let noticeId = "__transport_truncation_notice__";
|
|
2490
|
+
while (usedBlockIds.has(noticeId)) noticeId = `_${noticeId}`;
|
|
2491
|
+
const makeNotice = (droppedBlocks2) => ({
|
|
2492
|
+
blockId: noticeId,
|
|
2493
|
+
kind: "status",
|
|
2494
|
+
statusType: "notice",
|
|
2495
|
+
text: droppedBlocks2 > 0 ? `[Content truncated for remote display] ${droppedBlocks2} earlier blocks omitted.` : "[Content truncated for remote display]",
|
|
2496
|
+
level: "warning",
|
|
2497
|
+
isComplete: true,
|
|
2498
|
+
createdAt: boundedTurn.startedAt,
|
|
2499
|
+
updatedAt: boundedTurn.updatedAt,
|
|
2500
|
+
metadata: {
|
|
2501
|
+
[TRANSPORT_NOTICE_METADATA_KEY]: true,
|
|
2502
|
+
omittedBlocks: droppedBlocks2
|
|
2014
2503
|
}
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2504
|
+
});
|
|
2505
|
+
const maximumDrops = knownDrops + boundedEntries.length;
|
|
2506
|
+
const reserveNotice = needsNotice ? makeNotice(maximumDrops) : void 0;
|
|
2507
|
+
let retainedBytes = serializedBytes(build([], reserveNotice, maximumDrops));
|
|
2508
|
+
let retainedCount = 0;
|
|
2509
|
+
let retainedStart = boundedEntries.length;
|
|
2510
|
+
for (let index = boundedEntries.length - 1; index >= 0; index--) {
|
|
2511
|
+
const entry = boundedEntries[index];
|
|
2512
|
+
const addedBytes = utf8Size(JSON.stringify(entry.id)) + utf8Size(JSON.stringify(entry.id)) + 1 + serializedBytes(entry.block) + (reserveNotice || retainedCount > 0 ? 2 : 0);
|
|
2513
|
+
if (retainedBytes + addedBytes > budget) break;
|
|
2514
|
+
retainedBytes += addedBytes;
|
|
2515
|
+
retainedCount++;
|
|
2516
|
+
retainedStart = index;
|
|
2517
|
+
}
|
|
2518
|
+
let retained = boundedEntries.slice(retainedStart);
|
|
2519
|
+
if (retained.length === 0 && boundedEntries.length > 0) {
|
|
2520
|
+
const newest = boundedEntries.at(-1);
|
|
2521
|
+
retained = [{ ...newest, block: boundSnapshotBlock(newest.block, 32 * 1024) }];
|
|
2522
|
+
}
|
|
2523
|
+
const droppedBlocks = knownDrops + boundedEntries.length - retained.length;
|
|
2524
|
+
const notice = needsNotice ? makeNotice(droppedBlocks) : void 0;
|
|
2525
|
+
let result = build(retained, notice, droppedBlocks);
|
|
2526
|
+
if (serializedBytes(result) > budget) {
|
|
2527
|
+
result = build([], makeNotice(maximumDrops), maximumDrops);
|
|
2528
|
+
}
|
|
2529
|
+
return result;
|
|
2530
|
+
}
|
|
2531
|
+
function minimalTransportSnapshot(snapshot, truncatedTurns) {
|
|
2532
|
+
const originalTurnId = snapshot.turnOrder.at(-1);
|
|
2533
|
+
const originalTurn = originalTurnId ? snapshot.turns[originalTurnId] : void 0;
|
|
2534
|
+
const sessionId = truncateSnapshotText(snapshot.sessionId, MAX_SNAPSHOT_IDENTIFIER_BYTES);
|
|
2535
|
+
if (!originalTurnId || !originalTurn) {
|
|
2536
|
+
return {
|
|
2537
|
+
schemaVersion: snapshot.schemaVersion,
|
|
2538
|
+
sessionId,
|
|
2539
|
+
turnOrder: [],
|
|
2540
|
+
turns: {},
|
|
2541
|
+
...truncatedTurns === void 0 ? {} : { truncatedTurns },
|
|
2542
|
+
...snapshot.truncatedTurnIds === void 0 ? {} : { truncatedTurnIds: snapshot.truncatedTurnIds }
|
|
2019
2543
|
};
|
|
2020
2544
|
}
|
|
2021
|
-
|
|
2022
|
-
const
|
|
2023
|
-
|
|
2024
|
-
|
|
2545
|
+
const turnId = truncateSnapshotText(originalTurnId, MAX_SNAPSHOT_IDENTIFIER_BYTES) || "__transport_truncated_turn__";
|
|
2546
|
+
const blockId = "__transport_truncation_notice__";
|
|
2547
|
+
const startedAt = truncateSnapshotText(
|
|
2548
|
+
originalTurn.startedAt,
|
|
2549
|
+
MAX_SNAPSHOT_TIMESTAMP_BYTES
|
|
2550
|
+
);
|
|
2551
|
+
const updatedAt = truncateSnapshotText(
|
|
2552
|
+
originalTurn.updatedAt,
|
|
2553
|
+
MAX_SNAPSHOT_TIMESTAMP_BYTES
|
|
2554
|
+
);
|
|
2555
|
+
return {
|
|
2556
|
+
schemaVersion: snapshot.schemaVersion,
|
|
2557
|
+
sessionId,
|
|
2558
|
+
turnOrder: [turnId],
|
|
2559
|
+
turns: {
|
|
2560
|
+
[turnId]: {
|
|
2561
|
+
turnId,
|
|
2562
|
+
role: originalTurn.role,
|
|
2563
|
+
startedAt,
|
|
2564
|
+
updatedAt,
|
|
2565
|
+
endedAt: originalTurn.endedAt === void 0 ? void 0 : truncateSnapshotText(originalTurn.endedAt, MAX_SNAPSHOT_TIMESTAMP_BYTES),
|
|
2566
|
+
isComplete: originalTurn.isComplete,
|
|
2567
|
+
blockOrder: [blockId],
|
|
2568
|
+
blocks: {
|
|
2569
|
+
[blockId]: {
|
|
2570
|
+
blockId,
|
|
2571
|
+
kind: "status",
|
|
2572
|
+
statusType: "notice",
|
|
2573
|
+
text: "[Content truncated for remote display]",
|
|
2574
|
+
level: "warning",
|
|
2575
|
+
isComplete: true,
|
|
2576
|
+
createdAt: startedAt,
|
|
2577
|
+
updatedAt,
|
|
2578
|
+
metadata: {
|
|
2579
|
+
[TRANSPORT_NOTICE_METADATA_KEY]: true,
|
|
2580
|
+
omittedBlocks: 0
|
|
2581
|
+
},
|
|
2582
|
+
truncation: {
|
|
2583
|
+
reason: "transport_limit",
|
|
2584
|
+
keptBytes: 0,
|
|
2585
|
+
didWarnUser: true
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
},
|
|
2591
|
+
...truncatedTurns === void 0 ? {} : { truncatedTurns },
|
|
2592
|
+
...snapshot.truncatedTurnIds === void 0 ? {} : { truncatedTurnIds: snapshot.truncatedTurnIds }
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2595
|
+
function mergeTruncatedTurnIds(existing, dropped) {
|
|
2596
|
+
if ((existing?.length ?? 0) === 0 && dropped.length === 0) return void 0;
|
|
2597
|
+
const newestFirst = [];
|
|
2598
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2599
|
+
const candidates = [...existing ?? [], ...dropped];
|
|
2600
|
+
let bytes = 2;
|
|
2601
|
+
for (let index = candidates.length - 1; index >= 0; index--) {
|
|
2602
|
+
const id = candidates[index];
|
|
2603
|
+
if (seen.has(id)) continue;
|
|
2604
|
+
const idBytes = utf8Size(JSON.stringify(id));
|
|
2605
|
+
const added = idBytes + (newestFirst.length > 0 ? 1 : 0);
|
|
2606
|
+
if (bytes + added > MAX_TRUNCATED_TURN_ID_BYTES) break;
|
|
2607
|
+
bytes += added;
|
|
2608
|
+
seen.add(id);
|
|
2609
|
+
newestFirst.push(id);
|
|
2610
|
+
}
|
|
2611
|
+
return newestFirst.reverse();
|
|
2612
|
+
}
|
|
2613
|
+
function trimSnapshot(snapshot, maxTurns = MAX_SNAPSHOT_TURNS) {
|
|
2614
|
+
let currentSnapshot = snapshot;
|
|
2615
|
+
const originalLength = snapshot.turnOrder.length;
|
|
2616
|
+
const sizeBudget = MAX_SNAPSHOT_SIZE_BYTES - SNAPSHOT_FIELD_RESERVE;
|
|
2617
|
+
const countFloor = Math.max(0, originalLength - Math.max(1, maxTurns));
|
|
2618
|
+
const alreadyWithinCount = countFloor === 0;
|
|
2619
|
+
const originalSize = alreadyWithinCount ? serializedBytes(snapshot) : Number.POSITIVE_INFINITY;
|
|
2620
|
+
let knownCurrentSize = alreadyWithinCount ? originalSize : void 0;
|
|
2621
|
+
if (!alreadyWithinCount || originalSize > sizeBudget) {
|
|
2622
|
+
const shell = {
|
|
2623
|
+
...snapshot,
|
|
2624
|
+
turnOrder: [],
|
|
2625
|
+
turns: {}
|
|
2626
|
+
};
|
|
2627
|
+
let retainedBytes = serializedBytes(shell);
|
|
2628
|
+
let retainedOrderCount = 0;
|
|
2629
|
+
let retainedTurnCount = 0;
|
|
2630
|
+
let retainedStart = originalLength;
|
|
2631
|
+
for (let index = originalLength - 1; index >= countFloor; index--) {
|
|
2632
|
+
const id = snapshot.turnOrder[index];
|
|
2633
|
+
const turn = snapshot.turns[id];
|
|
2634
|
+
const addedOrderBytes = utf8Size(JSON.stringify(id)) + (retainedOrderCount > 0 ? 1 : 0);
|
|
2635
|
+
const addedTurnBytes = turn ? utf8Size(JSON.stringify(id)) + 1 + serializedBytes(turn) + (retainedTurnCount > 0 ? 1 : 0) : 0;
|
|
2636
|
+
if (retainedBytes + addedOrderBytes + addedTurnBytes > sizeBudget) break;
|
|
2637
|
+
retainedBytes += addedOrderBytes + addedTurnBytes;
|
|
2638
|
+
retainedOrderCount++;
|
|
2639
|
+
if (turn) retainedTurnCount++;
|
|
2640
|
+
retainedStart = index;
|
|
2641
|
+
}
|
|
2642
|
+
if (retainedStart === originalLength && originalLength > 0) {
|
|
2643
|
+
retainedStart = originalLength - 1;
|
|
2644
|
+
}
|
|
2645
|
+
const keptOrder = snapshot.turnOrder.slice(retainedStart);
|
|
2025
2646
|
const keptTurns = {};
|
|
2026
2647
|
for (const id of keptOrder) {
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
}
|
|
2648
|
+
const turn = snapshot.turns[id];
|
|
2649
|
+
if (turn) keptTurns[id] = turn;
|
|
2030
2650
|
}
|
|
2031
2651
|
currentSnapshot = {
|
|
2032
|
-
...
|
|
2652
|
+
...snapshot,
|
|
2033
2653
|
turnOrder: keptOrder,
|
|
2034
2654
|
turns: keptTurns
|
|
2035
2655
|
};
|
|
2036
|
-
|
|
2656
|
+
knownCurrentSize = void 0;
|
|
2657
|
+
}
|
|
2658
|
+
const soleTurnId = currentSnapshot.turnOrder.length === 1 ? currentSnapshot.turnOrder[0] : void 0;
|
|
2659
|
+
const soleTurn = soleTurnId ? currentSnapshot.turns[soleTurnId] : void 0;
|
|
2660
|
+
const omittedBlocks = soleTurn?.metadata?.[TRANSPORT_OMITTED_BLOCKS_KEY];
|
|
2661
|
+
const missingSyntheticNotice = typeof omittedBlocks === "number" && omittedBlocks > 0 && !soleTurn?.blockOrder.some((blockId) => soleTurn.blocks[blockId]?.metadata?.[TRANSPORT_NOTICE_METADATA_KEY] === true);
|
|
2662
|
+
if (currentSnapshot.turnOrder.length === 1 && ((knownCurrentSize ?? serializedBytes(currentSnapshot)) > sizeBudget || missingSyntheticNotice)) {
|
|
2663
|
+
currentSnapshot = boundSingleTurnSnapshot(currentSnapshot);
|
|
2664
|
+
knownCurrentSize = void 0;
|
|
2037
2665
|
}
|
|
2038
2666
|
const droppedThisCall = originalLength - currentSnapshot.turnOrder.length;
|
|
2039
2667
|
const truncatedTurns = (snapshot.truncatedTurns ?? 0) + droppedThisCall;
|
|
2040
2668
|
if (truncatedTurns > 0 && currentSnapshot.truncatedTurns !== truncatedTurns) {
|
|
2041
2669
|
currentSnapshot = { ...currentSnapshot, truncatedTurns };
|
|
2670
|
+
knownCurrentSize = void 0;
|
|
2671
|
+
}
|
|
2672
|
+
if (droppedThisCall > 0) {
|
|
2673
|
+
const truncatedTurnIds = mergeTruncatedTurnIds(
|
|
2674
|
+
snapshot.truncatedTurnIds,
|
|
2675
|
+
snapshot.turnOrder.slice(0, droppedThisCall)
|
|
2676
|
+
);
|
|
2677
|
+
currentSnapshot = { ...currentSnapshot, truncatedTurnIds };
|
|
2678
|
+
knownCurrentSize = void 0;
|
|
2679
|
+
}
|
|
2680
|
+
let finalSize = knownCurrentSize ?? serializedBytes(currentSnapshot);
|
|
2681
|
+
if (finalSize > MAX_SNAPSHOT_SIZE_BYTES) {
|
|
2682
|
+
currentSnapshot = minimalTransportSnapshot(
|
|
2683
|
+
currentSnapshot,
|
|
2684
|
+
truncatedTurns > 0 ? truncatedTurns : currentSnapshot.truncatedTurns
|
|
2685
|
+
);
|
|
2686
|
+
finalSize = serializedBytes(currentSnapshot);
|
|
2687
|
+
}
|
|
2688
|
+
if (finalSize > MAX_SNAPSHOT_SIZE_BYTES) {
|
|
2689
|
+
throw new Error("Transcript snapshot exceeded its UTF-8 byte budget");
|
|
2042
2690
|
}
|
|
2043
2691
|
return currentSnapshot;
|
|
2044
2692
|
}
|
|
2045
2693
|
|
|
2694
|
+
// src/transcript-wire.ts
|
|
2695
|
+
var import_node_crypto = require("crypto");
|
|
2696
|
+
var MAX_TRANSCRIPT_SNAPSHOT_BYTES = 448 * 1024;
|
|
2697
|
+
var MAX_TRANSCRIPT_OP_BATCH_BYTES = 192 * 1024;
|
|
2698
|
+
var MAX_TRANSCRIPT_TURN_BYTES = 160 * 1024;
|
|
2699
|
+
var MAX_TRANSCRIPT_FIELD_BYTES = 128 * 1024;
|
|
2700
|
+
var MAX_TOOL_RESULT_BYTES = 32 * 1024;
|
|
2701
|
+
var MAX_TOOL_INPUT_BYTES = 16 * 1024;
|
|
2702
|
+
var MAX_IDENTIFIER_BYTES = 4 * 1024;
|
|
2703
|
+
var MAX_TIMESTAMP_BYTES = 1024;
|
|
2704
|
+
var TRUNCATION_MARKER = "\n\n[Content truncated for remote display]\n\n";
|
|
2705
|
+
function utf8Bytes(value) {
|
|
2706
|
+
const serialized = JSON.stringify(value);
|
|
2707
|
+
return Buffer.byteLength(serialized ?? "", "utf8");
|
|
2708
|
+
}
|
|
2709
|
+
function stringBytes(value) {
|
|
2710
|
+
return Buffer.byteLength(value, "utf8");
|
|
2711
|
+
}
|
|
2712
|
+
function prefixWithinBytes2(value, maxBytes) {
|
|
2713
|
+
let low = 0;
|
|
2714
|
+
let high = value.length;
|
|
2715
|
+
while (low < high) {
|
|
2716
|
+
const mid = Math.ceil((low + high) / 2);
|
|
2717
|
+
if (stringBytes(value.slice(0, mid)) <= maxBytes) low = mid;
|
|
2718
|
+
else high = mid - 1;
|
|
2719
|
+
}
|
|
2720
|
+
if (low > 0 && /[\uD800-\uDBFF]/.test(value[low - 1] ?? "")) low--;
|
|
2721
|
+
return value.slice(0, low);
|
|
2722
|
+
}
|
|
2723
|
+
function suffixWithinBytes2(value, maxBytes) {
|
|
2724
|
+
let low = 0;
|
|
2725
|
+
let high = value.length;
|
|
2726
|
+
while (low < high) {
|
|
2727
|
+
const count = Math.ceil((low + high) / 2);
|
|
2728
|
+
if (stringBytes(value.slice(value.length - count)) <= maxBytes) low = count;
|
|
2729
|
+
else high = count - 1;
|
|
2730
|
+
}
|
|
2731
|
+
let start = value.length - low;
|
|
2732
|
+
if (start < value.length && /[\uDC00-\uDFFF]/.test(value[start] ?? "")) start++;
|
|
2733
|
+
return value.slice(start);
|
|
2734
|
+
}
|
|
2735
|
+
function truncateUtf8ForWire(value, maxBytes) {
|
|
2736
|
+
if (stringBytes(value) <= maxBytes) return value;
|
|
2737
|
+
const markerBytes = stringBytes(TRUNCATION_MARKER);
|
|
2738
|
+
if (maxBytes <= markerBytes) return prefixWithinBytes2(TRUNCATION_MARKER, maxBytes);
|
|
2739
|
+
const available = maxBytes - markerBytes;
|
|
2740
|
+
const head = prefixWithinBytes2(value, Math.floor(available * 0.75));
|
|
2741
|
+
const tail = suffixWithinBytes2(value, available - stringBytes(head));
|
|
2742
|
+
let result = `${head}${TRUNCATION_MARKER}${tail}`;
|
|
2743
|
+
if (stringBytes(result) > maxBytes) {
|
|
2744
|
+
result = `${prefixWithinBytes2(head, available)}${TRUNCATION_MARKER}`;
|
|
2745
|
+
}
|
|
2746
|
+
return result;
|
|
2747
|
+
}
|
|
2748
|
+
function boundedToolInput(input) {
|
|
2749
|
+
if (!input) return void 0;
|
|
2750
|
+
try {
|
|
2751
|
+
if (utf8Bytes(input) <= MAX_TOOL_INPUT_BYTES) return input;
|
|
2752
|
+
} catch {
|
|
2753
|
+
}
|
|
2754
|
+
return { notice: "Tool input truncated for remote display" };
|
|
2755
|
+
}
|
|
2756
|
+
function boundTool(tool) {
|
|
2757
|
+
return {
|
|
2758
|
+
...tool,
|
|
2759
|
+
name: truncateUtf8ForWire(tool.name, 2 * 1024),
|
|
2760
|
+
input: boundedToolInput(tool.input),
|
|
2761
|
+
result: typeof tool.result === "string" ? truncateUtf8ForWire(tool.result, MAX_TOOL_RESULT_BYTES) : void 0
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
function minimalMarkdownTurn(turn) {
|
|
2765
|
+
return {
|
|
2766
|
+
turnId: truncateUtf8ForWire(turn.turnId, 1024),
|
|
2767
|
+
role: turn.role,
|
|
2768
|
+
content: "[Content truncated for remote display]",
|
|
2769
|
+
timestamp: truncateUtf8ForWire(turn.timestamp, 256)
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
function boundMarkdownTurnForWire(turn) {
|
|
2773
|
+
let bounded = {
|
|
2774
|
+
...turn,
|
|
2775
|
+
turnId: truncateUtf8ForWire(turn.turnId, MAX_IDENTIFIER_BYTES),
|
|
2776
|
+
content: truncateUtf8ForWire(turn.content, MAX_TRANSCRIPT_FIELD_BYTES),
|
|
2777
|
+
tools: turn.tools?.slice(-12).map(boundTool),
|
|
2778
|
+
timestamp: truncateUtf8ForWire(turn.timestamp, MAX_TIMESTAMP_BYTES)
|
|
2779
|
+
};
|
|
2780
|
+
while (utf8Bytes(bounded) > MAX_TRANSCRIPT_TURN_BYTES && (bounded.tools?.length ?? 0) > 0) {
|
|
2781
|
+
bounded = { ...bounded, tools: bounded.tools.slice(1) };
|
|
2782
|
+
}
|
|
2783
|
+
if (utf8Bytes(bounded) > MAX_TRANSCRIPT_TURN_BYTES) {
|
|
2784
|
+
bounded = {
|
|
2785
|
+
...bounded,
|
|
2786
|
+
content: truncateUtf8ForWire(bounded.content, 64 * 1024),
|
|
2787
|
+
tools: void 0
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
if (utf8Bytes(bounded) > MAX_TRANSCRIPT_TURN_BYTES) {
|
|
2791
|
+
bounded = minimalMarkdownTurn(bounded);
|
|
2792
|
+
}
|
|
2793
|
+
if (utf8Bytes(bounded) > MAX_TRANSCRIPT_TURN_BYTES) {
|
|
2794
|
+
throw new Error("Legacy transcript turn exceeded its UTF-8 byte budget");
|
|
2795
|
+
}
|
|
2796
|
+
return bounded;
|
|
2797
|
+
}
|
|
2798
|
+
function boundMarkdownTurnsForWire(turns, maxBytes = MAX_TRANSCRIPT_SNAPSHOT_BYTES) {
|
|
2799
|
+
const bounded = turns.map(boundMarkdownTurnForWire);
|
|
2800
|
+
const emptySnapshotBytes = utf8Bytes({ turns: [] });
|
|
2801
|
+
let retainedBytes = emptySnapshotBytes;
|
|
2802
|
+
let retainedCount = 0;
|
|
2803
|
+
let start = bounded.length;
|
|
2804
|
+
for (let index = bounded.length - 1; index >= 0; index--) {
|
|
2805
|
+
const addedBytes = utf8Bytes(bounded[index]) + (retainedCount > 0 ? 1 : 0);
|
|
2806
|
+
if (retainedBytes + addedBytes > maxBytes) break;
|
|
2807
|
+
retainedBytes += addedBytes;
|
|
2808
|
+
retainedCount++;
|
|
2809
|
+
start = index;
|
|
2810
|
+
}
|
|
2811
|
+
let retained = bounded.slice(start);
|
|
2812
|
+
if (retained.length === 0 && bounded.length > 0) {
|
|
2813
|
+
retained = [minimalMarkdownTurn(bounded.at(-1))];
|
|
2814
|
+
}
|
|
2815
|
+
if (utf8Bytes({ turns: retained }) > maxBytes) {
|
|
2816
|
+
throw new Error("Legacy transcript snapshot exceeded its UTF-8 byte budget");
|
|
2817
|
+
}
|
|
2818
|
+
return retained;
|
|
2819
|
+
}
|
|
2820
|
+
function withTruncation(block, originalBytes, keptBytes) {
|
|
2821
|
+
return {
|
|
2822
|
+
...block,
|
|
2823
|
+
truncation: {
|
|
2824
|
+
reason: "transport_limit",
|
|
2825
|
+
originalBytes,
|
|
2826
|
+
keptBytes,
|
|
2827
|
+
didWarnUser: true
|
|
2828
|
+
}
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
function boundIdentifier(rawId, context) {
|
|
2832
|
+
const existing = context.identifiers.get(rawId);
|
|
2833
|
+
if (existing !== void 0) return existing;
|
|
2834
|
+
if (stringBytes(rawId) <= MAX_IDENTIFIER_BYTES) {
|
|
2835
|
+
context.identifiers.set(rawId, rawId);
|
|
2836
|
+
return rawId;
|
|
2837
|
+
}
|
|
2838
|
+
const suffix = `~${(0, import_node_crypto.createHash)("sha256").update(rawId).digest("hex").slice(0, 20)}`;
|
|
2839
|
+
const bounded = `${prefixWithinBytes2(
|
|
2840
|
+
rawId,
|
|
2841
|
+
MAX_IDENTIFIER_BYTES - stringBytes(suffix)
|
|
2842
|
+
)}${suffix}`;
|
|
2843
|
+
context.identifiers.set(rawId, bounded);
|
|
2844
|
+
return bounded;
|
|
2845
|
+
}
|
|
2846
|
+
function boundBlock(block, boundedBlockId = truncateUtf8ForWire(block.blockId, MAX_IDENTIFIER_BYTES)) {
|
|
2847
|
+
const originalBytes = utf8Bytes(block);
|
|
2848
|
+
let bounded;
|
|
2849
|
+
switch (block.kind) {
|
|
2850
|
+
case "text":
|
|
2851
|
+
case "reasoning":
|
|
2852
|
+
bounded = { ...block, text: truncateUtf8ForWire(block.text, MAX_TRANSCRIPT_FIELD_BYTES) };
|
|
2853
|
+
break;
|
|
2854
|
+
case "tool": {
|
|
2855
|
+
let input = block.input;
|
|
2856
|
+
if (input !== void 0) {
|
|
2857
|
+
try {
|
|
2858
|
+
if (utf8Bytes(input) > MAX_TOOL_INPUT_BYTES) {
|
|
2859
|
+
input = "[Tool input truncated for remote display]";
|
|
2860
|
+
}
|
|
2861
|
+
} catch {
|
|
2862
|
+
input = "[Tool input truncated for remote display]";
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
bounded = {
|
|
2866
|
+
...block,
|
|
2867
|
+
callId: truncateUtf8ForWire(block.callId, MAX_IDENTIFIER_BYTES),
|
|
2868
|
+
toolName: truncateUtf8ForWire(block.toolName, 2 * 1024),
|
|
2869
|
+
input,
|
|
2870
|
+
outputText: block.outputText === void 0 ? void 0 : truncateUtf8ForWire(block.outputText, MAX_TRANSCRIPT_FIELD_BYTES),
|
|
2871
|
+
errorText: block.errorText === void 0 ? void 0 : truncateUtf8ForWire(block.errorText, MAX_TOOL_RESULT_BYTES),
|
|
2872
|
+
outputMimeType: block.outputMimeType === void 0 ? void 0 : truncateUtf8ForWire(block.outputMimeType, 2 * 1024)
|
|
2873
|
+
};
|
|
2874
|
+
break;
|
|
2875
|
+
}
|
|
2876
|
+
case "status":
|
|
2877
|
+
bounded = {
|
|
2878
|
+
...block,
|
|
2879
|
+
statusType: truncateUtf8ForWire(block.statusType, 2 * 1024),
|
|
2880
|
+
text: block.text === void 0 ? void 0 : truncateUtf8ForWire(block.text, MAX_TOOL_RESULT_BYTES)
|
|
2881
|
+
};
|
|
2882
|
+
break;
|
|
2883
|
+
case "unknown":
|
|
2884
|
+
bounded = {
|
|
2885
|
+
...block,
|
|
2886
|
+
rawKind: truncateUtf8ForWire(block.rawKind, 2 * 1024),
|
|
2887
|
+
rawPayload: block.rawPayload === void 0 ? void 0 : "[Unknown payload truncated for remote display]",
|
|
2888
|
+
fallbackText: block.fallbackText === void 0 ? void 0 : truncateUtf8ForWire(block.fallbackText, MAX_TOOL_RESULT_BYTES)
|
|
2889
|
+
};
|
|
2890
|
+
break;
|
|
2891
|
+
case "attachment":
|
|
2892
|
+
bounded = {
|
|
2893
|
+
...block,
|
|
2894
|
+
mediaType: truncateUtf8ForWire(block.mediaType, 2 * 1024),
|
|
2895
|
+
uri: block.uri === void 0 ? void 0 : truncateUtf8ForWire(block.uri, 8 * 1024),
|
|
2896
|
+
path: block.path === void 0 ? void 0 : truncateUtf8ForWire(block.path, 8 * 1024),
|
|
2897
|
+
name: block.name === void 0 ? void 0 : truncateUtf8ForWire(block.name, 2 * 1024)
|
|
2898
|
+
};
|
|
2899
|
+
break;
|
|
2900
|
+
}
|
|
2901
|
+
bounded = {
|
|
2902
|
+
...bounded,
|
|
2903
|
+
blockId: boundedBlockId,
|
|
2904
|
+
createdAt: truncateUtf8ForWire(block.createdAt, MAX_TIMESTAMP_BYTES),
|
|
2905
|
+
updatedAt: truncateUtf8ForWire(block.updatedAt, MAX_TIMESTAMP_BYTES),
|
|
2906
|
+
metadata: void 0
|
|
2907
|
+
};
|
|
2908
|
+
if (utf8Bytes(bounded) < originalBytes) {
|
|
2909
|
+
bounded = withTruncation(bounded, originalBytes, utf8Bytes(bounded));
|
|
2910
|
+
}
|
|
2911
|
+
return bounded;
|
|
2912
|
+
}
|
|
2913
|
+
function boundOp(op, context) {
|
|
2914
|
+
switch (op.op) {
|
|
2915
|
+
case "append_text":
|
|
2916
|
+
case "append_tool_output":
|
|
2917
|
+
return {
|
|
2918
|
+
...op,
|
|
2919
|
+
turnId: boundIdentifier(op.turnId, context),
|
|
2920
|
+
blockId: boundIdentifier(op.blockId, context),
|
|
2921
|
+
text: truncateUtf8ForWire(op.text, MAX_TRANSCRIPT_FIELD_BYTES)
|
|
2922
|
+
};
|
|
2923
|
+
case "insert_block":
|
|
2924
|
+
return {
|
|
2925
|
+
...op,
|
|
2926
|
+
turnId: boundIdentifier(op.turnId, context),
|
|
2927
|
+
afterBlockId: op.afterBlockId === void 0 ? void 0 : boundIdentifier(op.afterBlockId, context),
|
|
2928
|
+
block: boundBlock(
|
|
2929
|
+
op.block,
|
|
2930
|
+
boundIdentifier(op.block.blockId, context)
|
|
2931
|
+
)
|
|
2932
|
+
};
|
|
2933
|
+
case "replace_block":
|
|
2934
|
+
return {
|
|
2935
|
+
...op,
|
|
2936
|
+
turnId: boundIdentifier(op.turnId, context),
|
|
2937
|
+
block: boundBlock(
|
|
2938
|
+
op.block,
|
|
2939
|
+
boundIdentifier(op.block.blockId, context)
|
|
2940
|
+
)
|
|
2941
|
+
};
|
|
2942
|
+
case "upsert_turn": {
|
|
2943
|
+
return {
|
|
2944
|
+
...op,
|
|
2945
|
+
turn: {
|
|
2946
|
+
...op.turn,
|
|
2947
|
+
turnId: boundIdentifier(op.turn.turnId, context),
|
|
2948
|
+
startedAt: truncateUtf8ForWire(op.turn.startedAt, MAX_TIMESTAMP_BYTES),
|
|
2949
|
+
updatedAt: truncateUtf8ForWire(op.turn.updatedAt, MAX_TIMESTAMP_BYTES),
|
|
2950
|
+
endedAt: op.turn.endedAt === void 0 ? void 0 : truncateUtf8ForWire(op.turn.endedAt, MAX_TIMESTAMP_BYTES),
|
|
2951
|
+
agent: op.turn.agent === void 0 ? void 0 : {
|
|
2952
|
+
vendor: op.turn.agent.vendor === void 0 ? void 0 : truncateUtf8ForWire(op.turn.agent.vendor, 2 * 1024),
|
|
2953
|
+
agentType: op.turn.agent.agentType === void 0 ? void 0 : truncateUtf8ForWire(op.turn.agent.agentType, 2 * 1024),
|
|
2954
|
+
name: op.turn.agent.name === void 0 ? void 0 : truncateUtf8ForWire(op.turn.agent.name, 2 * 1024),
|
|
2955
|
+
version: op.turn.agent.version === void 0 ? void 0 : truncateUtf8ForWire(op.turn.agent.version, 2 * 1024),
|
|
2956
|
+
transcriptSource: op.turn.agent.transcriptSource
|
|
2957
|
+
},
|
|
2958
|
+
metadata: void 0,
|
|
2959
|
+
blocks: void 0,
|
|
2960
|
+
blockOrder: void 0
|
|
2961
|
+
}
|
|
2962
|
+
};
|
|
2963
|
+
}
|
|
2964
|
+
case "set_tool_state":
|
|
2965
|
+
return {
|
|
2966
|
+
...op,
|
|
2967
|
+
turnId: boundIdentifier(op.turnId, context),
|
|
2968
|
+
blockId: boundIdentifier(op.blockId, context),
|
|
2969
|
+
errorText: op.errorText === void 0 ? void 0 : truncateUtf8ForWire(op.errorText, MAX_TOOL_RESULT_BYTES)
|
|
2970
|
+
};
|
|
2971
|
+
case "complete_block":
|
|
2972
|
+
return {
|
|
2973
|
+
...op,
|
|
2974
|
+
turnId: boundIdentifier(op.turnId, context),
|
|
2975
|
+
blockId: boundIdentifier(op.blockId, context),
|
|
2976
|
+
updatedAt: truncateUtf8ForWire(op.updatedAt, MAX_TIMESTAMP_BYTES)
|
|
2977
|
+
};
|
|
2978
|
+
case "complete_turn":
|
|
2979
|
+
return {
|
|
2980
|
+
...op,
|
|
2981
|
+
turnId: boundIdentifier(op.turnId, context),
|
|
2982
|
+
endedAt: truncateUtf8ForWire(op.endedAt, MAX_TIMESTAMP_BYTES),
|
|
2983
|
+
updatedAt: truncateUtf8ForWire(op.updatedAt, MAX_TIMESTAMP_BYTES)
|
|
2984
|
+
};
|
|
2985
|
+
default:
|
|
2986
|
+
return op;
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
function expandBoundOp(op, context) {
|
|
2990
|
+
if (op.op !== "upsert_turn" || !op.turn.blocks) return [boundOp(op, context)];
|
|
2991
|
+
const order = op.turn.blockOrder ?? Object.keys(op.turn.blocks);
|
|
2992
|
+
const base = boundOp(op, context);
|
|
2993
|
+
const boundedTurnId = base.op === "upsert_turn" ? base.turn.turnId : truncateUtf8ForWire(op.turn.turnId, MAX_IDENTIFIER_BYTES);
|
|
2994
|
+
const expanded = [base];
|
|
2995
|
+
for (const blockId of order) {
|
|
2996
|
+
const block = op.turn.blocks[blockId];
|
|
2997
|
+
if (!block) continue;
|
|
2998
|
+
expanded.push({
|
|
2999
|
+
op: "insert_block",
|
|
3000
|
+
// Use exactly the same bounded identifier as the base upsert so an
|
|
3001
|
+
// adversarial long ID cannot orphan its expanded blocks.
|
|
3002
|
+
turnId: boundedTurnId,
|
|
3003
|
+
block: boundBlock(
|
|
3004
|
+
block,
|
|
3005
|
+
boundIdentifier(block.blockId, context)
|
|
3006
|
+
)
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
return expanded;
|
|
3010
|
+
}
|
|
3011
|
+
function forceBoundOp(op, maxBytes) {
|
|
3012
|
+
if (utf8Bytes(op) <= maxBytes) return op;
|
|
3013
|
+
if (op.op === "insert_block" || op.op === "replace_block") {
|
|
3014
|
+
return {
|
|
3015
|
+
...op,
|
|
3016
|
+
block: {
|
|
3017
|
+
blockId: truncateUtf8ForWire(op.block.blockId, MAX_IDENTIFIER_BYTES),
|
|
3018
|
+
kind: "status",
|
|
3019
|
+
statusType: "notice",
|
|
3020
|
+
text: "Block content truncated for remote display",
|
|
3021
|
+
level: "warning",
|
|
3022
|
+
isComplete: op.block.isComplete,
|
|
3023
|
+
createdAt: op.block.createdAt,
|
|
3024
|
+
updatedAt: op.block.updatedAt,
|
|
3025
|
+
truncation: {
|
|
3026
|
+
reason: "transport_limit",
|
|
3027
|
+
originalBytes: utf8Bytes(op.block),
|
|
3028
|
+
keptBytes: 0,
|
|
3029
|
+
didWarnUser: true
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
};
|
|
3033
|
+
}
|
|
3034
|
+
if (op.op === "append_text" || op.op === "append_tool_output") {
|
|
3035
|
+
return { ...op, text: truncateUtf8ForWire(op.text, 32 * 1024) };
|
|
3036
|
+
}
|
|
3037
|
+
return op;
|
|
3038
|
+
}
|
|
3039
|
+
function boundTranscriptOpsForWire(ops, maxBytes = MAX_TRANSCRIPT_OP_BATCH_BYTES) {
|
|
3040
|
+
const idContext = {
|
|
3041
|
+
identifiers: /* @__PURE__ */ new Map()
|
|
3042
|
+
};
|
|
3043
|
+
const batches = [];
|
|
3044
|
+
let current = [];
|
|
3045
|
+
let currentBytes = 2;
|
|
3046
|
+
for (const rawOp of ops) {
|
|
3047
|
+
for (const expanded of expandBoundOp(rawOp, idContext)) {
|
|
3048
|
+
const op = forceBoundOp(expanded, maxBytes);
|
|
3049
|
+
const opBytes = utf8Bytes(op);
|
|
3050
|
+
if (opBytes + 2 > maxBytes) continue;
|
|
3051
|
+
const addedBytes = opBytes + (current.length > 0 ? 1 : 0);
|
|
3052
|
+
if (current.length > 0 && currentBytes + addedBytes > maxBytes) {
|
|
3053
|
+
batches.push(current);
|
|
3054
|
+
current = [];
|
|
3055
|
+
currentBytes = 2;
|
|
3056
|
+
}
|
|
3057
|
+
current.push(op);
|
|
3058
|
+
currentBytes += opBytes + (current.length > 1 ? 1 : 0);
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
if (current.length > 0) batches.push(current);
|
|
3062
|
+
if (batches.some((batch) => utf8Bytes(batch) > maxBytes)) {
|
|
3063
|
+
throw new Error("Transcript wire batch exceeded its UTF-8 byte budget");
|
|
3064
|
+
}
|
|
3065
|
+
return batches;
|
|
3066
|
+
}
|
|
3067
|
+
|
|
2046
3068
|
// src/native-stream-manager.ts
|
|
2047
3069
|
var import_child_process = require("child_process");
|
|
2048
3070
|
|
|
@@ -2580,6 +3602,9 @@ var import_ws = __toESM(require("ws"));
|
|
|
2580
3602
|
var BACKPRESSURE_HIGH_WATER = 256 * 1024;
|
|
2581
3603
|
var BACKPRESSURE_LOW_WATER = 64 * 1024;
|
|
2582
3604
|
var MAX_PENDING_QUEUE = 500;
|
|
3605
|
+
var MAX_PENDING_BYTES = 4 * 1024 * 1024;
|
|
3606
|
+
var CONTROL_FRAME_RESERVE = 32;
|
|
3607
|
+
var CONTROL_BYTE_RESERVE = 256 * 1024;
|
|
2583
3608
|
var DRAIN_INTERVAL_MS = 50;
|
|
2584
3609
|
function sanitizeForJson(str) {
|
|
2585
3610
|
return str.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD");
|
|
@@ -2633,6 +3658,7 @@ function flushStreamingRedaction(sessionId) {
|
|
|
2633
3658
|
return redactionEnabled ? redactSecrets(tail) : tail;
|
|
2634
3659
|
}
|
|
2635
3660
|
var pendingQueue = [];
|
|
3661
|
+
var pendingQueueBytes = 0;
|
|
2636
3662
|
var drainTimer = null;
|
|
2637
3663
|
var drainWs = null;
|
|
2638
3664
|
var droppedFrames = 0;
|
|
@@ -2640,10 +3666,17 @@ var PRESSURE_HIGH = 0.75;
|
|
|
2640
3666
|
var PRESSURE_LOW = 0.25;
|
|
2641
3667
|
var pressureIsHigh = false;
|
|
2642
3668
|
function getQueuePressure() {
|
|
2643
|
-
const
|
|
3669
|
+
const frameLevel = pendingQueue.length / MAX_PENDING_QUEUE;
|
|
3670
|
+
const byteLevel = pendingQueueBytes / MAX_PENDING_BYTES;
|
|
3671
|
+
const level = Math.max(frameLevel, byteLevel);
|
|
2644
3672
|
if (level >= PRESSURE_HIGH) pressureIsHigh = true;
|
|
2645
3673
|
else if (level < PRESSURE_LOW) pressureIsHigh = false;
|
|
2646
|
-
return {
|
|
3674
|
+
return {
|
|
3675
|
+
level,
|
|
3676
|
+
queuedFrames: pendingQueue.length,
|
|
3677
|
+
queuedBytes: pendingQueueBytes,
|
|
3678
|
+
isHigh: pressureIsHigh
|
|
3679
|
+
};
|
|
2647
3680
|
}
|
|
2648
3681
|
function getDroppedFrameCount() {
|
|
2649
3682
|
return droppedFrames;
|
|
@@ -2654,13 +3687,133 @@ function drainQueue() {
|
|
|
2654
3687
|
return;
|
|
2655
3688
|
}
|
|
2656
3689
|
while (pendingQueue.length > 0 && drainWs.bufferedAmount < BACKPRESSURE_LOW_WATER) {
|
|
2657
|
-
const
|
|
2658
|
-
|
|
3690
|
+
const pending = pendingQueue.shift();
|
|
3691
|
+
pendingQueueBytes -= pending.bytes;
|
|
3692
|
+
drainWs.send(pending.frame);
|
|
2659
3693
|
}
|
|
2660
3694
|
if (pendingQueue.length === 0) {
|
|
2661
3695
|
stopDrainTimer();
|
|
2662
3696
|
}
|
|
2663
3697
|
}
|
|
3698
|
+
function classifyPendingFrame(frame, group, data, hints) {
|
|
3699
|
+
const sessionId = hints?.sessionId ?? (typeof data.sessionId === "string" ? data.sessionId : void 0);
|
|
3700
|
+
const isMarkdown = data.type === "markdown" || hints?.markdownSubtype !== void 0;
|
|
3701
|
+
const subtype = hints?.markdownSubtype ?? (typeof data.subtype === "string" ? data.subtype : void 0);
|
|
3702
|
+
const isResetSnapshot = isMarkdown && subtype === "snapshot" && (hints?.reset === true || data.reset === true);
|
|
3703
|
+
const replaceableSnapshot = isMarkdown && subtype === "snapshot" && !isResetSnapshot;
|
|
3704
|
+
const markdownSchemaVersion = isMarkdown ? hints?.markdownSchemaVersion ?? (data.schemaVersion === 2 ? 2 : 1) : void 0;
|
|
3705
|
+
const transcriptOrderBarrier = isMarkdown && !replaceableSnapshot;
|
|
3706
|
+
const protectedOrderBarrier = isResetSnapshot || data.type === "stream-reset" || data.type === "stream-started" || data.type === "stream-stopped";
|
|
3707
|
+
const protectedBarrierKind = isResetSnapshot ? "markdown-reset" : data.type === "stream-reset" ? "stream-reset" : data.type === "stream-started" ? "stream-started" : data.type === "stream-stopped" ? "stream-stopped" : void 0;
|
|
3708
|
+
let priority = 2 /* Control */;
|
|
3709
|
+
if (data.type === "output" || isMarkdown && subtype === "delta") {
|
|
3710
|
+
priority = 0 /* Bulk */;
|
|
3711
|
+
} else if (replaceableSnapshot) {
|
|
3712
|
+
priority = 1 /* Snapshot */;
|
|
3713
|
+
}
|
|
3714
|
+
return {
|
|
3715
|
+
frame,
|
|
3716
|
+
bytes: Buffer.byteLength(frame, "utf8"),
|
|
3717
|
+
group,
|
|
3718
|
+
sessionId,
|
|
3719
|
+
priority,
|
|
3720
|
+
replaceableSnapshot,
|
|
3721
|
+
markdownSchemaVersion,
|
|
3722
|
+
// Encrypted markdown frames have no visible subtype, so treat them as a
|
|
3723
|
+
// barrier. Guessing that one is replaceable could reorder an encrypted
|
|
3724
|
+
// reset, status, or delta and corrupt transcript state.
|
|
3725
|
+
transcriptOrderBarrier,
|
|
3726
|
+
protectedOrderBarrier,
|
|
3727
|
+
protectedBarrierKind
|
|
3728
|
+
};
|
|
3729
|
+
}
|
|
3730
|
+
function obsoleteSnapshotIndices(incoming) {
|
|
3731
|
+
const indices = /* @__PURE__ */ new Set();
|
|
3732
|
+
if (!incoming.replaceableSnapshot || !incoming.sessionId) return indices;
|
|
3733
|
+
for (let i = pendingQueue.length - 1; i >= 0; i--) {
|
|
3734
|
+
const queued = pendingQueue[i];
|
|
3735
|
+
if (queued.group !== incoming.group || queued.sessionId !== incoming.sessionId) {
|
|
3736
|
+
continue;
|
|
3737
|
+
}
|
|
3738
|
+
if (queued.transcriptOrderBarrier) break;
|
|
3739
|
+
if (queued.replaceableSnapshot && queued.markdownSchemaVersion === incoming.markdownSchemaVersion) {
|
|
3740
|
+
indices.add(i);
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
return indices;
|
|
3744
|
+
}
|
|
3745
|
+
function supersededProtectedBarrierIndices(incoming) {
|
|
3746
|
+
const indices = /* @__PURE__ */ new Set();
|
|
3747
|
+
if (!incoming.protectedBarrierKind || !incoming.sessionId) return indices;
|
|
3748
|
+
for (let i = pendingQueue.length - 1; i >= 0; i--) {
|
|
3749
|
+
const queued = pendingQueue[i];
|
|
3750
|
+
if (queued.group !== incoming.group || queued.sessionId !== incoming.sessionId) {
|
|
3751
|
+
continue;
|
|
3752
|
+
}
|
|
3753
|
+
if (queued.protectedBarrierKind === incoming.protectedBarrierKind) {
|
|
3754
|
+
indices.add(i);
|
|
3755
|
+
continue;
|
|
3756
|
+
}
|
|
3757
|
+
break;
|
|
3758
|
+
}
|
|
3759
|
+
return indices;
|
|
3760
|
+
}
|
|
3761
|
+
function recordDroppedFrame() {
|
|
3762
|
+
droppedFrames++;
|
|
3763
|
+
if (droppedFrames % 100 === 1) {
|
|
3764
|
+
console.warn(
|
|
3765
|
+
`[rAgent] Backpressure: dropped ${droppedFrames} frames (queue limit ${MAX_PENDING_QUEUE} frames / ${MAX_PENDING_BYTES} bytes)`
|
|
3766
|
+
);
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
function findEvictionCandidate(incomingPriority, excluded) {
|
|
3770
|
+
for (let priority = 0 /* Bulk */; priority <= incomingPriority; priority++) {
|
|
3771
|
+
const index = pendingQueue.findIndex(
|
|
3772
|
+
(queued, queuedIndex) => !excluded.has(queuedIndex) && !queued.protectedOrderBarrier && queued.priority === priority
|
|
3773
|
+
);
|
|
3774
|
+
if (index >= 0) return index;
|
|
3775
|
+
}
|
|
3776
|
+
return -1;
|
|
3777
|
+
}
|
|
3778
|
+
function enqueuePendingFrame(pending) {
|
|
3779
|
+
if (pending.bytes > MAX_PENDING_BYTES) {
|
|
3780
|
+
recordDroppedFrame();
|
|
3781
|
+
return false;
|
|
3782
|
+
}
|
|
3783
|
+
const obsolete = obsoleteSnapshotIndices(pending);
|
|
3784
|
+
const supersededBarriers = supersededProtectedBarrierIndices(pending);
|
|
3785
|
+
const coalesced = /* @__PURE__ */ new Set([...obsolete, ...supersededBarriers]);
|
|
3786
|
+
const removable = new Set(coalesced);
|
|
3787
|
+
const frameCapacity = pending.priority === 0 /* Bulk */ ? MAX_PENDING_QUEUE - CONTROL_FRAME_RESERVE : MAX_PENDING_QUEUE;
|
|
3788
|
+
const byteCapacity = pending.priority === 0 /* Bulk */ ? MAX_PENDING_BYTES - CONTROL_BYTE_RESERVE : MAX_PENDING_BYTES;
|
|
3789
|
+
let projectedLength = pendingQueue.length - removable.size;
|
|
3790
|
+
let projectedBytes = pendingQueueBytes;
|
|
3791
|
+
for (const index of removable) projectedBytes -= pendingQueue[index].bytes;
|
|
3792
|
+
while (projectedLength >= frameCapacity || projectedBytes + pending.bytes > byteCapacity) {
|
|
3793
|
+
let candidate = findEvictionCandidate(pending.priority, removable);
|
|
3794
|
+
if (candidate < 0 && pending.protectedOrderBarrier) {
|
|
3795
|
+
candidate = pendingQueue.findIndex(
|
|
3796
|
+
(queued, queuedIndex) => !removable.has(queuedIndex) && queued.protectedOrderBarrier
|
|
3797
|
+
);
|
|
3798
|
+
}
|
|
3799
|
+
if (candidate < 0) {
|
|
3800
|
+
recordDroppedFrame();
|
|
3801
|
+
return false;
|
|
3802
|
+
}
|
|
3803
|
+
removable.add(candidate);
|
|
3804
|
+
projectedLength--;
|
|
3805
|
+
projectedBytes -= pendingQueue[candidate].bytes;
|
|
3806
|
+
}
|
|
3807
|
+
const indices = [...removable].sort((a, b) => b - a);
|
|
3808
|
+
for (const index of indices) {
|
|
3809
|
+
const [removed] = pendingQueue.splice(index, 1);
|
|
3810
|
+
pendingQueueBytes -= removed.bytes;
|
|
3811
|
+
if (!coalesced.has(index)) recordDroppedFrame();
|
|
3812
|
+
}
|
|
3813
|
+
pendingQueue.push(pending);
|
|
3814
|
+
pendingQueueBytes += pending.bytes;
|
|
3815
|
+
return true;
|
|
3816
|
+
}
|
|
2664
3817
|
function startDrainTimer(ws) {
|
|
2665
3818
|
drainWs = ws;
|
|
2666
3819
|
if (!drainTimer) {
|
|
@@ -2674,7 +3827,7 @@ function stopDrainTimer() {
|
|
|
2674
3827
|
}
|
|
2675
3828
|
drainWs = null;
|
|
2676
3829
|
}
|
|
2677
|
-
function sendToGroup(ws, group, data) {
|
|
3830
|
+
function sendToGroup(ws, group, data, hints) {
|
|
2678
3831
|
if (!group || ws.readyState !== import_ws.default.OPEN) return;
|
|
2679
3832
|
const sanitized = sanitizePayload(data);
|
|
2680
3833
|
const frame = JSON.stringify({
|
|
@@ -2684,20 +3837,20 @@ function sendToGroup(ws, group, data) {
|
|
|
2684
3837
|
data: sanitized,
|
|
2685
3838
|
noEcho: true
|
|
2686
3839
|
});
|
|
3840
|
+
const pending = classifyPendingFrame(frame, group, sanitized, hints);
|
|
3841
|
+
if (pending.bytes > MAX_PENDING_BYTES) {
|
|
3842
|
+
recordDroppedFrame();
|
|
3843
|
+
return;
|
|
3844
|
+
}
|
|
2687
3845
|
if (ws.bufferedAmount > BACKPRESSURE_HIGH_WATER) {
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
if (droppedFrames % 100 === 1) {
|
|
2692
|
-
console.warn(`[rAgent] Backpressure: dropped ${droppedFrames} frames (queue full at ${MAX_PENDING_QUEUE})`);
|
|
2693
|
-
}
|
|
3846
|
+
enqueuePendingFrame(pending);
|
|
3847
|
+
if (pendingQueue.length > 0) {
|
|
3848
|
+
startDrainTimer(ws);
|
|
2694
3849
|
}
|
|
2695
|
-
pendingQueue.push(frame);
|
|
2696
|
-
startDrainTimer(ws);
|
|
2697
3850
|
return;
|
|
2698
3851
|
}
|
|
2699
3852
|
if (pendingQueue.length > 0) {
|
|
2700
|
-
|
|
3853
|
+
enqueuePendingFrame(pending);
|
|
2701
3854
|
drainWs = ws;
|
|
2702
3855
|
drainQueue();
|
|
2703
3856
|
return;
|
|
@@ -2728,6 +3881,36 @@ function sanitizePayload(obj) {
|
|
|
2728
3881
|
var log7 = createLogger("transcript-watcher");
|
|
2729
3882
|
var cachedTicksPerSecond = null;
|
|
2730
3883
|
var transcriptDiscoveryCache = /* @__PURE__ */ new Map();
|
|
3884
|
+
var codexSubagentTranscriptCache = /* @__PURE__ */ new Map();
|
|
3885
|
+
function createTranscriptDiscoveryIndex() {
|
|
3886
|
+
return { candidates: /* @__PURE__ */ new Map() };
|
|
3887
|
+
}
|
|
3888
|
+
function indexedCandidates(index, key, scan) {
|
|
3889
|
+
const rank = () => scan().map((target) => {
|
|
3890
|
+
try {
|
|
3891
|
+
return { target, mtimeMs: fs2.statSync(target).mtimeMs };
|
|
3892
|
+
} catch {
|
|
3893
|
+
return null;
|
|
3894
|
+
}
|
|
3895
|
+
}).filter((entry) => entry !== null).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
3896
|
+
if (!index) return rank();
|
|
3897
|
+
const cached = index.candidates.get(key);
|
|
3898
|
+
if (cached) return cached;
|
|
3899
|
+
const candidates = rank();
|
|
3900
|
+
index.candidates.set(key, candidates);
|
|
3901
|
+
return candidates;
|
|
3902
|
+
}
|
|
3903
|
+
function getNewestIndexedPath(candidates, options) {
|
|
3904
|
+
for (const candidate of candidates) {
|
|
3905
|
+
if (options?.minMtimeMs !== void 0 && candidate.mtimeMs < options.minMtimeMs) {
|
|
3906
|
+
break;
|
|
3907
|
+
}
|
|
3908
|
+
if (options?.excludePaths?.has(candidate.target)) continue;
|
|
3909
|
+
if (options?.isEligible && !options.isEligible(candidate.target)) continue;
|
|
3910
|
+
return candidate.target;
|
|
3911
|
+
}
|
|
3912
|
+
return null;
|
|
3913
|
+
}
|
|
2731
3914
|
var ClaudeCodeParser = class {
|
|
2732
3915
|
name = "claude-code";
|
|
2733
3916
|
/** Maps tool_use id → tool name for matching results back to tools. */
|
|
@@ -2903,6 +4086,7 @@ var CodexCliParser = class {
|
|
|
2903
4086
|
if (!payload.call_id) return null;
|
|
2904
4087
|
const toolName = this.pendingTools.get(payload.call_id) ?? "tool";
|
|
2905
4088
|
this.pendingTools.delete(payload.call_id);
|
|
4089
|
+
const output = normalizeCodexFunctionCallOutput(payload.output);
|
|
2906
4090
|
return {
|
|
2907
4091
|
turnId: this.genTurnId(lineTimestamp),
|
|
2908
4092
|
role: "user",
|
|
@@ -2911,7 +4095,7 @@ var CodexCliParser = class {
|
|
|
2911
4095
|
{
|
|
2912
4096
|
name: toolName,
|
|
2913
4097
|
status: "completed",
|
|
2914
|
-
result:
|
|
4098
|
+
result: output || void 0
|
|
2915
4099
|
}
|
|
2916
4100
|
],
|
|
2917
4101
|
timestamp
|
|
@@ -3219,7 +4403,48 @@ function getNewestPath(paths, options) {
|
|
|
3219
4403
|
return null;
|
|
3220
4404
|
}
|
|
3221
4405
|
}).filter((entry) => entry !== null).filter((entry) => options?.minMtimeMs === void 0 || entry.mtimeMs >= options.minMtimeMs).filter((entry) => !options?.excludePaths?.has(entry.target)).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
3222
|
-
return ranked
|
|
4406
|
+
return ranked.find((entry) => options?.isEligible?.(entry.target) ?? true)?.target ?? null;
|
|
4407
|
+
}
|
|
4408
|
+
function isCodexSubagentTranscript(target) {
|
|
4409
|
+
const cached = codexSubagentTranscriptCache.get(target);
|
|
4410
|
+
if (cached !== void 0) return cached;
|
|
4411
|
+
let isSubagent = false;
|
|
4412
|
+
let classified = false;
|
|
4413
|
+
let fd = null;
|
|
4414
|
+
try {
|
|
4415
|
+
fd = fs2.openSync(target, "r");
|
|
4416
|
+
const buffer = Buffer.alloc(256 * 1024);
|
|
4417
|
+
const bytesRead = fs2.readSync(fd, buffer, 0, buffer.length, 0);
|
|
4418
|
+
const content = buffer.subarray(0, bytesRead).toString("utf8");
|
|
4419
|
+
const newline = content.indexOf("\n");
|
|
4420
|
+
if (newline < 0 && bytesRead === buffer.length) {
|
|
4421
|
+
throw new Error("session metadata exceeds bounded read");
|
|
4422
|
+
}
|
|
4423
|
+
const firstLine = newline >= 0 ? content.slice(0, newline) : content;
|
|
4424
|
+
const record = JSON.parse(firstLine);
|
|
4425
|
+
classified = true;
|
|
4426
|
+
const source = record.type === "session_meta" ? record.payload?.source : void 0;
|
|
4427
|
+
isSubagent = Boolean(
|
|
4428
|
+
source && typeof source === "object" && !Array.isArray(source) && Object.prototype.hasOwnProperty.call(source, "subagent")
|
|
4429
|
+
);
|
|
4430
|
+
} catch {
|
|
4431
|
+
isSubagent = false;
|
|
4432
|
+
} finally {
|
|
4433
|
+
if (fd !== null) {
|
|
4434
|
+
try {
|
|
4435
|
+
fs2.closeSync(fd);
|
|
4436
|
+
} catch {
|
|
4437
|
+
}
|
|
4438
|
+
}
|
|
4439
|
+
}
|
|
4440
|
+
if (classified) {
|
|
4441
|
+
if (codexSubagentTranscriptCache.size >= 5e3) {
|
|
4442
|
+
const oldest = codexSubagentTranscriptCache.keys().next().value;
|
|
4443
|
+
if (oldest) codexSubagentTranscriptCache.delete(oldest);
|
|
4444
|
+
}
|
|
4445
|
+
codexSubagentTranscriptCache.set(target, isSubagent);
|
|
4446
|
+
}
|
|
4447
|
+
return isSubagent;
|
|
3223
4448
|
}
|
|
3224
4449
|
function discoverTranscriptForPid(pid, agentType) {
|
|
3225
4450
|
const fdDir = `/proc/${pid}/fd`;
|
|
@@ -3235,7 +4460,7 @@ function discoverTranscriptForPid(pid, agentType) {
|
|
|
3235
4460
|
} catch {
|
|
3236
4461
|
}
|
|
3237
4462
|
}
|
|
3238
|
-
return getNewestPath(matches);
|
|
4463
|
+
return getNewestPath(matches, agentType === "Codex CLI" ? { isEligible: (target) => !isCodexSubagentTranscript(target) } : void 0);
|
|
3239
4464
|
} catch {
|
|
3240
4465
|
return null;
|
|
3241
4466
|
}
|
|
@@ -3366,10 +4591,10 @@ function discoverViaProc(rootPid, agentType) {
|
|
|
3366
4591
|
}
|
|
3367
4592
|
return getNewestPath(candidates);
|
|
3368
4593
|
}
|
|
3369
|
-
function discoverViaProcessCwd(pid, minMtimeMs, excludePaths) {
|
|
4594
|
+
function discoverViaProcessCwd(pid, minMtimeMs, excludePaths, discoveryIndex) {
|
|
3370
4595
|
try {
|
|
3371
4596
|
const processCwd = fs2.readlinkSync(`/proc/${pid}/cwd`);
|
|
3372
|
-
return processCwd ? discoverViaCwd(processCwd, pid, minMtimeMs, excludePaths) : null;
|
|
4597
|
+
return processCwd ? discoverViaCwd(processCwd, pid, minMtimeMs, excludePaths, discoveryIndex) : null;
|
|
3373
4598
|
} catch {
|
|
3374
4599
|
return null;
|
|
3375
4600
|
}
|
|
@@ -3397,83 +4622,108 @@ function resolveUserHome(pid) {
|
|
|
3397
4622
|
return null;
|
|
3398
4623
|
}
|
|
3399
4624
|
}
|
|
3400
|
-
function discoverCodexTranscriptFromHome(pid, minMtimeMs, excludePaths) {
|
|
4625
|
+
function discoverCodexTranscriptFromHome(pid, minMtimeMs, excludePaths, discoveryIndex) {
|
|
3401
4626
|
const userHome = resolveUserHome(pid);
|
|
3402
4627
|
if (!userHome) return null;
|
|
3403
4628
|
const sessionsDir = path2.join(userHome, ".codex", "sessions");
|
|
3404
4629
|
if (!fs2.existsSync(sessionsDir)) return null;
|
|
3405
|
-
const matches =
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
4630
|
+
const matches = indexedCandidates(
|
|
4631
|
+
discoveryIndex,
|
|
4632
|
+
`codex:${sessionsDir}`,
|
|
4633
|
+
() => {
|
|
4634
|
+
const found = [];
|
|
4635
|
+
const stack = [sessionsDir];
|
|
4636
|
+
let visited = 0;
|
|
4637
|
+
while (stack.length > 0 && visited < 3e3) {
|
|
4638
|
+
const current = stack.pop();
|
|
4639
|
+
if (!current) continue;
|
|
4640
|
+
visited++;
|
|
4641
|
+
let entries;
|
|
4642
|
+
try {
|
|
4643
|
+
entries = fs2.readdirSync(current, { withFileTypes: true });
|
|
4644
|
+
} catch {
|
|
4645
|
+
continue;
|
|
4646
|
+
}
|
|
4647
|
+
for (const entry of entries) {
|
|
4648
|
+
const fullPath = path2.join(current, entry.name);
|
|
4649
|
+
if (entry.isDirectory()) {
|
|
4650
|
+
stack.push(fullPath);
|
|
4651
|
+
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
4652
|
+
found.push(fullPath);
|
|
4653
|
+
}
|
|
4654
|
+
}
|
|
3424
4655
|
}
|
|
4656
|
+
return found;
|
|
3425
4657
|
}
|
|
3426
|
-
|
|
3427
|
-
return
|
|
4658
|
+
);
|
|
4659
|
+
return getNewestIndexedPath(matches, {
|
|
4660
|
+
minMtimeMs,
|
|
4661
|
+
excludePaths,
|
|
4662
|
+
isEligible: (target) => !isCodexSubagentTranscript(target)
|
|
4663
|
+
});
|
|
3428
4664
|
}
|
|
3429
|
-
function discoverGeminiTranscriptFromHome(pid, minMtimeMs, excludePaths) {
|
|
4665
|
+
function discoverGeminiTranscriptFromHome(pid, minMtimeMs, excludePaths, discoveryIndex) {
|
|
3430
4666
|
const userHome = resolveUserHome(pid);
|
|
3431
4667
|
if (!userHome) return null;
|
|
3432
4668
|
const tmpDir = path2.join(userHome, ".gemini", "tmp");
|
|
3433
4669
|
if (!fs2.existsSync(tmpDir)) return null;
|
|
3434
|
-
const matches =
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
4670
|
+
const matches = indexedCandidates(
|
|
4671
|
+
discoveryIndex,
|
|
4672
|
+
`gemini:${tmpDir}`,
|
|
4673
|
+
() => {
|
|
4674
|
+
const found = [];
|
|
4675
|
+
const stack = [tmpDir];
|
|
4676
|
+
let visited = 0;
|
|
4677
|
+
while (stack.length > 0 && visited < 2e3) {
|
|
4678
|
+
const current = stack.pop();
|
|
4679
|
+
if (!current) continue;
|
|
4680
|
+
visited++;
|
|
4681
|
+
let entries;
|
|
4682
|
+
try {
|
|
4683
|
+
entries = fs2.readdirSync(current, { withFileTypes: true });
|
|
4684
|
+
} catch {
|
|
4685
|
+
continue;
|
|
4686
|
+
}
|
|
4687
|
+
for (const entry of entries) {
|
|
4688
|
+
const fullPath = path2.join(current, entry.name);
|
|
4689
|
+
if (entry.isDirectory()) {
|
|
4690
|
+
stack.push(fullPath);
|
|
4691
|
+
} else if (entry.isFile() && entry.name.startsWith("session-") && entry.name.endsWith(".json") && fullPath.includes(`${path2.sep}chats${path2.sep}`)) {
|
|
4692
|
+
found.push(fullPath);
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
3453
4695
|
}
|
|
4696
|
+
return found;
|
|
3454
4697
|
}
|
|
3455
|
-
|
|
3456
|
-
return
|
|
4698
|
+
);
|
|
4699
|
+
return getNewestIndexedPath(matches, { minMtimeMs, excludePaths });
|
|
3457
4700
|
}
|
|
3458
|
-
function discoverAntigravityTranscriptFromHome(pid, minMtimeMs, excludePaths) {
|
|
4701
|
+
function discoverAntigravityTranscriptFromHome(pid, minMtimeMs, excludePaths, discoveryIndex) {
|
|
3459
4702
|
const userHome = resolveUserHome(pid);
|
|
3460
4703
|
if (!userHome) return null;
|
|
3461
4704
|
const brainDir = path2.join(userHome, ".gemini", "antigravity-cli", "brain");
|
|
3462
4705
|
if (!fs2.existsSync(brainDir)) return null;
|
|
3463
|
-
const matches =
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
4706
|
+
const matches = indexedCandidates(
|
|
4707
|
+
discoveryIndex,
|
|
4708
|
+
`antigravity:${brainDir}`,
|
|
4709
|
+
() => {
|
|
4710
|
+
const found = [];
|
|
4711
|
+
try {
|
|
4712
|
+
const folders = fs2.readdirSync(brainDir, { withFileTypes: true });
|
|
4713
|
+
for (const folder of folders) {
|
|
4714
|
+
if (folder.isDirectory()) {
|
|
4715
|
+
const transcriptPath = path2.join(brainDir, folder.name, ".system_generated", "logs", "transcript.jsonl");
|
|
4716
|
+
if (fs2.existsSync(transcriptPath)) {
|
|
4717
|
+
found.push(transcriptPath);
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
3471
4720
|
}
|
|
4721
|
+
} catch {
|
|
3472
4722
|
}
|
|
4723
|
+
return found;
|
|
3473
4724
|
}
|
|
3474
|
-
|
|
3475
|
-
}
|
|
3476
|
-
return getNewestPath(matches, { minMtimeMs, excludePaths });
|
|
4725
|
+
);
|
|
4726
|
+
return getNewestIndexedPath(matches, { minMtimeMs, excludePaths });
|
|
3477
4727
|
}
|
|
3478
4728
|
function discoverAntigravityViaCwd(paneCwd, pid, excludePaths) {
|
|
3479
4729
|
const userHome = resolveUserHome(pid);
|
|
@@ -3521,7 +4771,7 @@ function discoverAntigravityViaCwd(paneCwd, pid, excludePaths) {
|
|
|
3521
4771
|
}
|
|
3522
4772
|
return null;
|
|
3523
4773
|
}
|
|
3524
|
-
function discoverViaCwd(paneCwd, pid, minMtimeMs, excludePaths) {
|
|
4774
|
+
function discoverViaCwd(paneCwd, pid, minMtimeMs, excludePaths, discoveryIndex) {
|
|
3525
4775
|
const userHome = resolveUserHome(pid);
|
|
3526
4776
|
if (!userHome) return null;
|
|
3527
4777
|
const claudeProjectsDir = path2.join(userHome, ".claude", "projects");
|
|
@@ -3534,8 +4784,12 @@ function discoverViaCwd(paneCwd, pid, minMtimeMs, excludePaths) {
|
|
|
3534
4784
|
const projectDirs = Array.from(possibleCwds).map((cwd) => path2.join(claudeProjectsDir, cwd.replace(/\//g, "-"))).filter((projectDir, index, arr) => arr.indexOf(projectDir) === index).filter((projectDir) => fs2.existsSync(projectDir));
|
|
3535
4785
|
if (projectDirs.length === 0) return null;
|
|
3536
4786
|
try {
|
|
3537
|
-
const jsonlFiles = projectDirs.flatMap((projectDir) =>
|
|
3538
|
-
|
|
4787
|
+
const jsonlFiles = projectDirs.flatMap((projectDir) => indexedCandidates(
|
|
4788
|
+
discoveryIndex,
|
|
4789
|
+
`claude:${projectDir}`,
|
|
4790
|
+
() => fs2.readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).map((f) => path2.join(projectDir, f))
|
|
4791
|
+
));
|
|
4792
|
+
return getNewestIndexedPath(jsonlFiles, { minMtimeMs, excludePaths });
|
|
3539
4793
|
} catch {
|
|
3540
4794
|
return null;
|
|
3541
4795
|
}
|
|
@@ -3561,10 +4815,15 @@ function discoverTranscriptFile(sessionId, agentType, forceRefresh = false, excl
|
|
|
3561
4815
|
}
|
|
3562
4816
|
return result;
|
|
3563
4817
|
}
|
|
3564
|
-
function discoverTranscriptFileDetailed(sessionId, agentType, excludePaths) {
|
|
3565
|
-
return discoverTranscriptFileImpl(
|
|
4818
|
+
function discoverTranscriptFileDetailed(sessionId, agentType, excludePaths, discoveryIndex) {
|
|
4819
|
+
return discoverTranscriptFileImpl(
|
|
4820
|
+
sessionId,
|
|
4821
|
+
agentType,
|
|
4822
|
+
excludePaths,
|
|
4823
|
+
discoveryIndex
|
|
4824
|
+
);
|
|
3566
4825
|
}
|
|
3567
|
-
function discoverTranscriptFileImpl(sessionId, agentType, excludePaths) {
|
|
4826
|
+
function discoverTranscriptFileImpl(sessionId, agentType, excludePaths, discoveryIndex) {
|
|
3568
4827
|
const processPid = parseProcessPid(sessionId);
|
|
3569
4828
|
if (processPid) {
|
|
3570
4829
|
const t0 = performance.now();
|
|
@@ -3576,7 +4835,12 @@ function discoverTranscriptFileImpl(sessionId, agentType, excludePaths) {
|
|
|
3576
4835
|
if (agentType === "Claude Code") {
|
|
3577
4836
|
const t1 = performance.now();
|
|
3578
4837
|
const startMs = getProcessStartEpochMs(processPid);
|
|
3579
|
-
const cwdResult = discoverViaProcessCwd(
|
|
4838
|
+
const cwdResult = discoverViaProcessCwd(
|
|
4839
|
+
processPid,
|
|
4840
|
+
startMs ? startMs - 6e4 : void 0,
|
|
4841
|
+
excludePaths,
|
|
4842
|
+
discoveryIndex
|
|
4843
|
+
);
|
|
3580
4844
|
console.log(`[DISCOVER] Claude Code discovery for PID ${processPid} took ${(performance.now() - t1).toFixed(2)} ms`);
|
|
3581
4845
|
if (cwdResult) return { path: cwdResult, viaProc: false };
|
|
3582
4846
|
}
|
|
@@ -3585,7 +4849,12 @@ function discoverTranscriptFileImpl(sessionId, agentType, excludePaths) {
|
|
|
3585
4849
|
const startMs = getProcessStartEpochMs(processPid);
|
|
3586
4850
|
const command = getProcessCommand(processPid);
|
|
3587
4851
|
if (commandMatchesAgent(command, agentType)) {
|
|
3588
|
-
const codexResult = discoverCodexTranscriptFromHome(
|
|
4852
|
+
const codexResult = discoverCodexTranscriptFromHome(
|
|
4853
|
+
processPid,
|
|
4854
|
+
startMs ? startMs - 6e4 : void 0,
|
|
4855
|
+
excludePaths,
|
|
4856
|
+
discoveryIndex
|
|
4857
|
+
);
|
|
3589
4858
|
console.log(`[DISCOVER] Codex CLI discovery for PID ${processPid} took ${(performance.now() - t1).toFixed(2)} ms`);
|
|
3590
4859
|
if (codexResult) return { path: codexResult, viaProc: false };
|
|
3591
4860
|
}
|
|
@@ -3593,7 +4862,12 @@ function discoverTranscriptFileImpl(sessionId, agentType, excludePaths) {
|
|
|
3593
4862
|
if (agentType === "Gemini CLI") {
|
|
3594
4863
|
const t1 = performance.now();
|
|
3595
4864
|
const startMs = getProcessStartEpochMs(processPid);
|
|
3596
|
-
const geminiResult = discoverGeminiTranscriptFromHome(
|
|
4865
|
+
const geminiResult = discoverGeminiTranscriptFromHome(
|
|
4866
|
+
processPid,
|
|
4867
|
+
startMs ? startMs - 6e4 : void 0,
|
|
4868
|
+
excludePaths,
|
|
4869
|
+
discoveryIndex
|
|
4870
|
+
);
|
|
3597
4871
|
console.log(`[DISCOVER] Gemini CLI discovery for PID ${processPid} took ${(performance.now() - t1).toFixed(2)} ms`);
|
|
3598
4872
|
if (geminiResult) return { path: geminiResult, viaProc: false };
|
|
3599
4873
|
}
|
|
@@ -3611,7 +4885,12 @@ function discoverTranscriptFileImpl(sessionId, agentType, excludePaths) {
|
|
|
3611
4885
|
}
|
|
3612
4886
|
} catch {
|
|
3613
4887
|
}
|
|
3614
|
-
const antigravityResult = discoverAntigravityTranscriptFromHome(
|
|
4888
|
+
const antigravityResult = discoverAntigravityTranscriptFromHome(
|
|
4889
|
+
processPid,
|
|
4890
|
+
startMs ? startMs - 6e4 : void 0,
|
|
4891
|
+
excludePaths,
|
|
4892
|
+
discoveryIndex
|
|
4893
|
+
);
|
|
3615
4894
|
console.log(`[DISCOVER] Antigravity CLI discovery (home path) for PID ${processPid} took ${(performance.now() - t1).toFixed(2)} ms`);
|
|
3616
4895
|
if (antigravityResult) return { path: antigravityResult, viaProc: false };
|
|
3617
4896
|
}
|
|
@@ -3646,7 +4925,8 @@ function discoverTranscriptFileImpl(sessionId, agentType, excludePaths) {
|
|
|
3646
4925
|
info.cwd,
|
|
3647
4926
|
info.pid,
|
|
3648
4927
|
info.startEpochMs ? info.startEpochMs - 6e4 : void 0,
|
|
3649
|
-
excludePaths
|
|
4928
|
+
excludePaths,
|
|
4929
|
+
discoveryIndex
|
|
3650
4930
|
);
|
|
3651
4931
|
if (cwdResult) return { path: cwdResult, viaProc: false };
|
|
3652
4932
|
}
|
|
@@ -3657,7 +4937,13 @@ function discoverTranscriptFileImpl(sessionId, agentType, excludePaths) {
|
|
|
3657
4937
|
{ env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
|
|
3658
4938
|
).trim();
|
|
3659
4939
|
if (paneCwd) {
|
|
3660
|
-
const cwdResult = discoverViaCwd(
|
|
4940
|
+
const cwdResult = discoverViaCwd(
|
|
4941
|
+
paneCwd,
|
|
4942
|
+
panePid ?? void 0,
|
|
4943
|
+
void 0,
|
|
4944
|
+
excludePaths,
|
|
4945
|
+
discoveryIndex
|
|
4946
|
+
);
|
|
3661
4947
|
if (cwdResult) return { path: cwdResult, viaProc: false };
|
|
3662
4948
|
}
|
|
3663
4949
|
} catch {
|
|
@@ -3669,14 +4955,20 @@ function discoverTranscriptFileImpl(sessionId, agentType, excludePaths) {
|
|
|
3669
4955
|
agentInfos.map((info) => discoverCodexTranscriptFromHome(
|
|
3670
4956
|
info.pid,
|
|
3671
4957
|
info.startEpochMs ? info.startEpochMs - 6e4 : void 0,
|
|
3672
|
-
excludePaths
|
|
4958
|
+
excludePaths,
|
|
4959
|
+
discoveryIndex
|
|
3673
4960
|
)).filter((value) => Boolean(value)),
|
|
3674
4961
|
{ excludePaths }
|
|
3675
4962
|
);
|
|
3676
4963
|
if (newestForAgent) return { path: newestForAgent, viaProc: false };
|
|
3677
4964
|
}
|
|
3678
4965
|
if (agentType === "Gemini CLI") {
|
|
3679
|
-
const geminiResult = discoverGeminiTranscriptFromHome(
|
|
4966
|
+
const geminiResult = discoverGeminiTranscriptFromHome(
|
|
4967
|
+
panePid ?? void 0,
|
|
4968
|
+
void 0,
|
|
4969
|
+
excludePaths,
|
|
4970
|
+
discoveryIndex
|
|
4971
|
+
);
|
|
3680
4972
|
if (geminiResult) return { path: geminiResult, viaProc: false };
|
|
3681
4973
|
}
|
|
3682
4974
|
if (agentType === "Antigravity CLI") {
|
|
@@ -3703,7 +4995,8 @@ function discoverTranscriptFileImpl(sessionId, agentType, excludePaths) {
|
|
|
3703
4995
|
agentInfos.map((info) => discoverAntigravityTranscriptFromHome(
|
|
3704
4996
|
info.pid,
|
|
3705
4997
|
info.startEpochMs ? info.startEpochMs - 6e4 : void 0,
|
|
3706
|
-
excludePaths
|
|
4998
|
+
excludePaths,
|
|
4999
|
+
discoveryIndex
|
|
3707
5000
|
)).filter((value) => Boolean(value)),
|
|
3708
5001
|
{ excludePaths }
|
|
3709
5002
|
);
|
|
@@ -3713,11 +5006,17 @@ function discoverTranscriptFileImpl(sessionId, agentType, excludePaths) {
|
|
|
3713
5006
|
}
|
|
3714
5007
|
return { path: null, viaProc: false };
|
|
3715
5008
|
}
|
|
3716
|
-
var
|
|
5009
|
+
var MAX_JSONL_RECORD_BYTES = 8 * 1024 * 1024;
|
|
5010
|
+
var READ_CHUNK_BYTES = 256 * 1024;
|
|
5011
|
+
var MAX_READ_BYTES_PER_PASS = 4 * 1024 * 1024;
|
|
5012
|
+
var ASYNC_REPLAY_THRESHOLD_BYTES = 1024 * 1024;
|
|
5013
|
+
var ASYNC_REPLAY_YIELD_RECORDS = 64;
|
|
3717
5014
|
var MAX_REPLAY_TURNS = 1e3;
|
|
3718
5015
|
var POLL_INTERVAL_MS = 800;
|
|
3719
5016
|
var DOC_READ_DEBOUNCE_MS = 200;
|
|
5017
|
+
var V2_OP_BATCH_WINDOW_MS = 35;
|
|
3720
5018
|
var REDISCOVERY_INTERVAL_MS = 5e3;
|
|
5019
|
+
var MIN_REDISCOVERY_STAGGER_MS = 250;
|
|
3721
5020
|
var MARKDOWN_RESYNC_DRAIN_MS = 300;
|
|
3722
5021
|
var ENABLE_RETRY_DELAYS_MS = [1e3, 2e3, 5e3];
|
|
3723
5022
|
var TranscriptWatcher = class {
|
|
@@ -3728,6 +5027,8 @@ var TranscriptWatcher = class {
|
|
|
3728
5027
|
offset = 0;
|
|
3729
5028
|
inode = 0;
|
|
3730
5029
|
partialLine = "";
|
|
5030
|
+
discardingOversizedLine = false;
|
|
5031
|
+
decoder = new import_string_decoder.StringDecoder("utf8");
|
|
3731
5032
|
seq = 0;
|
|
3732
5033
|
turns = [];
|
|
3733
5034
|
/** V2 normalized snapshot state. */
|
|
@@ -3735,6 +5036,17 @@ var TranscriptWatcher = class {
|
|
|
3735
5036
|
watcher = null;
|
|
3736
5037
|
pollTimer = null;
|
|
3737
5038
|
docReadDebounceTimer = null;
|
|
5039
|
+
pendingOpsTimer = null;
|
|
5040
|
+
immediateReadTimer = null;
|
|
5041
|
+
pendingOps = [];
|
|
5042
|
+
pendingOpsSeq = 0;
|
|
5043
|
+
replaying = false;
|
|
5044
|
+
suppressReplayV1Deltas = false;
|
|
5045
|
+
suppressReplayV2Deltas = false;
|
|
5046
|
+
replayInProgress = false;
|
|
5047
|
+
replayReadPending = false;
|
|
5048
|
+
replayGeneration = 0;
|
|
5049
|
+
replayPromise = Promise.resolve();
|
|
3738
5050
|
stopped = false;
|
|
3739
5051
|
subscriberCount = 0;
|
|
3740
5052
|
documentFingerprint = "";
|
|
@@ -3780,7 +5092,11 @@ var TranscriptWatcher = class {
|
|
|
3780
5092
|
}
|
|
3781
5093
|
/** Stop watching. */
|
|
3782
5094
|
stop() {
|
|
5095
|
+
this.flushPendingOps();
|
|
3783
5096
|
this.stopped = true;
|
|
5097
|
+
this.replayGeneration++;
|
|
5098
|
+
this.replayInProgress = false;
|
|
5099
|
+
this.replayReadPending = false;
|
|
3784
5100
|
if (this.watcher) {
|
|
3785
5101
|
this.watcher.close();
|
|
3786
5102
|
this.watcher = null;
|
|
@@ -3793,13 +5109,23 @@ var TranscriptWatcher = class {
|
|
|
3793
5109
|
clearTimeout(this.docReadDebounceTimer);
|
|
3794
5110
|
this.docReadDebounceTimer = null;
|
|
3795
5111
|
}
|
|
5112
|
+
if (this.pendingOpsTimer) {
|
|
5113
|
+
clearTimeout(this.pendingOpsTimer);
|
|
5114
|
+
this.pendingOpsTimer = null;
|
|
5115
|
+
}
|
|
5116
|
+
if (this.immediateReadTimer) {
|
|
5117
|
+
clearTimeout(this.immediateReadTimer);
|
|
5118
|
+
this.immediateReadTimer = null;
|
|
5119
|
+
}
|
|
3796
5120
|
}
|
|
3797
|
-
/** Get accumulated turns for replay (up to MAX_REPLAY_TURNS).
|
|
5121
|
+
/** Get accumulated turns for replay (up to MAX_REPLAY_TURNS).
|
|
5122
|
+
* `fromSeq` is retained for API compatibility only: V1 sequence numbers no
|
|
5123
|
+
* longer map to array indexes now that V2 operations are batched. A V1 sync
|
|
5124
|
+
* must therefore use the full bounded, idempotent snapshot. */
|
|
3798
5125
|
getReplayTurns(fromSeq) {
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
return this.turns.slice(-MAX_REPLAY_TURNS);
|
|
5126
|
+
this.flushPendingOps();
|
|
5127
|
+
void fromSeq;
|
|
5128
|
+
return boundMarkdownTurnsForWire(this.turns.slice(-MAX_REPLAY_TURNS));
|
|
3803
5129
|
}
|
|
3804
5130
|
/** Get current sequence number. */
|
|
3805
5131
|
get currentSeq() {
|
|
@@ -3807,24 +5133,310 @@ var TranscriptWatcher = class {
|
|
|
3807
5133
|
}
|
|
3808
5134
|
/** Get the V2 normalized snapshot. */
|
|
3809
5135
|
getSnapshot() {
|
|
5136
|
+
this.flushPendingOps();
|
|
3810
5137
|
return trimSnapshot(this.snapshot);
|
|
3811
5138
|
}
|
|
5139
|
+
queueV2Ops(ops) {
|
|
5140
|
+
if (ops.length === 0) return;
|
|
5141
|
+
const wireBatches = boundTranscriptOpsForWire(ops);
|
|
5142
|
+
for (const batch of wireBatches) {
|
|
5143
|
+
if (this.pendingOps.length > 0 && utf8Bytes([...this.pendingOps, ...batch]) > MAX_TRANSCRIPT_OP_BATCH_BYTES) {
|
|
5144
|
+
this.flushPendingOps();
|
|
5145
|
+
}
|
|
5146
|
+
if (this.pendingOps.length === 0) {
|
|
5147
|
+
this.seq++;
|
|
5148
|
+
this.pendingOpsSeq = this.seq;
|
|
5149
|
+
}
|
|
5150
|
+
this.pendingOps.push(...batch);
|
|
5151
|
+
if (wireBatches.length > 1) this.flushPendingOps();
|
|
5152
|
+
}
|
|
5153
|
+
if (ops.some((op) => this.isCompletionBarrier(op))) {
|
|
5154
|
+
this.flushPendingOps();
|
|
5155
|
+
return;
|
|
5156
|
+
}
|
|
5157
|
+
if (!this.pendingOpsTimer) {
|
|
5158
|
+
this.pendingOpsTimer = setTimeout(() => {
|
|
5159
|
+
this.pendingOpsTimer = null;
|
|
5160
|
+
this.flushPendingOps();
|
|
5161
|
+
}, V2_OP_BATCH_WINDOW_MS);
|
|
5162
|
+
if (typeof this.pendingOpsTimer.unref === "function") {
|
|
5163
|
+
this.pendingOpsTimer.unref();
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5166
|
+
}
|
|
5167
|
+
isCompletionBarrier(op) {
|
|
5168
|
+
if (op.op === "complete_block" || op.op === "complete_turn") return true;
|
|
5169
|
+
return op.op === "set_tool_state" && (op.isComplete === true || op.state === "completed" || op.state === "error" || op.state === "cancelled");
|
|
5170
|
+
}
|
|
5171
|
+
flushPendingOps() {
|
|
5172
|
+
if (this.pendingOpsTimer) {
|
|
5173
|
+
clearTimeout(this.pendingOpsTimer);
|
|
5174
|
+
this.pendingOpsTimer = null;
|
|
5175
|
+
}
|
|
5176
|
+
if (this.pendingOps.length === 0) return;
|
|
5177
|
+
const ops = this.pendingOps;
|
|
5178
|
+
const seq = this.pendingOpsSeq;
|
|
5179
|
+
this.pendingOps = [];
|
|
5180
|
+
this.pendingOpsSeq = 0;
|
|
5181
|
+
try {
|
|
5182
|
+
this.snapshot = applyOps(this.snapshot, ops);
|
|
5183
|
+
this.callbacks.onOps?.(ops, seq);
|
|
5184
|
+
} catch (error) {
|
|
5185
|
+
this.reportParserFailure(
|
|
5186
|
+
this.parserV2?.name ?? "transcript-v2",
|
|
5187
|
+
"line",
|
|
5188
|
+
error
|
|
5189
|
+
);
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
/**
|
|
5193
|
+
* Parser/schema drift must never terminate the connector: terminal transport
|
|
5194
|
+
* and transcript rendering share this process. Report the bad record without
|
|
5195
|
+
* including transcript content, then let the watcher continue with later
|
|
5196
|
+
* records.
|
|
5197
|
+
*/
|
|
5198
|
+
reportParserFailure(parserName, phase, error) {
|
|
5199
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
5200
|
+
const message = `${parserName} failed to parse transcript ${phase}: ${detail}`;
|
|
5201
|
+
try {
|
|
5202
|
+
if (this.callbacks.onError) {
|
|
5203
|
+
this.callbacks.onError(message);
|
|
5204
|
+
} else {
|
|
5205
|
+
log7.warn("transcript parser failure; record skipped", {
|
|
5206
|
+
parser: parserName,
|
|
5207
|
+
phase,
|
|
5208
|
+
error: detail
|
|
5209
|
+
});
|
|
5210
|
+
}
|
|
5211
|
+
} catch (callbackError) {
|
|
5212
|
+
log7.warn("transcript parser failure callback also failed", {
|
|
5213
|
+
parser: parserName,
|
|
5214
|
+
phase,
|
|
5215
|
+
error: detail,
|
|
5216
|
+
callbackError: callbackError instanceof Error ? callbackError.message : String(callbackError)
|
|
5217
|
+
});
|
|
5218
|
+
}
|
|
5219
|
+
}
|
|
3812
5220
|
/** Read and replay the full transcript from the start of the file. */
|
|
3813
5221
|
replayFromStart() {
|
|
5222
|
+
this.flushPendingOps();
|
|
3814
5223
|
if (this.parser.parseDocument) {
|
|
3815
5224
|
this.readDocument(true);
|
|
3816
5225
|
return;
|
|
3817
5226
|
}
|
|
5227
|
+
const wantsV1 = this.callbacks.shouldProcessV1?.() ?? true;
|
|
5228
|
+
const wantsV2 = this.callbacks.shouldProcessV2?.() ?? Boolean(this.callbacks.onOps || this.callbacks.onSnapshot);
|
|
5229
|
+
let stat;
|
|
5230
|
+
try {
|
|
5231
|
+
stat = fs2.statSync(this.filePath);
|
|
5232
|
+
} catch {
|
|
5233
|
+
return;
|
|
5234
|
+
}
|
|
5235
|
+
const canEmitOneSnapshot = (!wantsV1 || Boolean(this.callbacks.onTurnsSnapshot)) && (!wantsV2 || Boolean(this.callbacks.onSnapshot));
|
|
5236
|
+
if (stat.size >= ASYNC_REPLAY_THRESHOLD_BYTES && canEmitOneSnapshot) {
|
|
5237
|
+
const generation = ++this.replayGeneration;
|
|
5238
|
+
this.replayInProgress = true;
|
|
5239
|
+
this.replayPromise = this.replayLineHistoryAsync(
|
|
5240
|
+
stat.size,
|
|
5241
|
+
stat.ino,
|
|
5242
|
+
generation,
|
|
5243
|
+
wantsV1,
|
|
5244
|
+
wantsV2
|
|
5245
|
+
).catch((error) => {
|
|
5246
|
+
this.reportParserFailure(
|
|
5247
|
+
this.parserV2?.name ?? this.parser.name,
|
|
5248
|
+
"document",
|
|
5249
|
+
error
|
|
5250
|
+
);
|
|
5251
|
+
}).finally(() => {
|
|
5252
|
+
if (generation !== this.replayGeneration) return;
|
|
5253
|
+
this.replayInProgress = false;
|
|
5254
|
+
if (this.replayReadPending && !this.stopped) {
|
|
5255
|
+
this.replayReadPending = false;
|
|
5256
|
+
this.scheduleImmediateRead();
|
|
5257
|
+
}
|
|
5258
|
+
});
|
|
5259
|
+
return;
|
|
5260
|
+
}
|
|
5261
|
+
let content;
|
|
5262
|
+
try {
|
|
5263
|
+
content = fs2.readFileSync(this.filePath, "utf-8");
|
|
5264
|
+
} catch {
|
|
5265
|
+
return;
|
|
5266
|
+
}
|
|
3818
5267
|
const savedOffset = this.offset;
|
|
3819
|
-
this.offset =
|
|
5268
|
+
this.offset = Buffer.byteLength(content);
|
|
3820
5269
|
this.partialLine = "";
|
|
5270
|
+
this.discardingOversizedLine = false;
|
|
5271
|
+
this.decoder = new import_string_decoder.StringDecoder("utf8");
|
|
5272
|
+
this.turns = [];
|
|
3821
5273
|
this.snapshot = createEmptySnapshot(this.snapshot.sessionId);
|
|
3822
|
-
this.
|
|
3823
|
-
this.
|
|
5274
|
+
this.replaying = true;
|
|
5275
|
+
this.suppressReplayV1Deltas = wantsV1 && Boolean(this.callbacks.onTurnsSnapshot);
|
|
5276
|
+
this.suppressReplayV2Deltas = Boolean(
|
|
5277
|
+
wantsV2 && this.parserV2?.parseDocument && this.callbacks.onSnapshot
|
|
5278
|
+
);
|
|
5279
|
+
try {
|
|
5280
|
+
if (wantsV1) this.processChunk(content);
|
|
5281
|
+
} finally {
|
|
5282
|
+
this.replaying = false;
|
|
5283
|
+
this.suppressReplayV1Deltas = false;
|
|
5284
|
+
this.suppressReplayV2Deltas = false;
|
|
5285
|
+
}
|
|
5286
|
+
let rebuiltV2 = false;
|
|
5287
|
+
if (wantsV2 && this.parserV2?.parseDocument && this.callbacks.onSnapshot) {
|
|
5288
|
+
rebuiltV2 = this.rebuildV2SnapshotFromDocument(content);
|
|
5289
|
+
}
|
|
5290
|
+
const emitV1Snapshot = wantsV1 && Boolean(this.callbacks.onTurnsSnapshot);
|
|
5291
|
+
const emitV2Snapshot = rebuiltV2 && Boolean(this.callbacks.onSnapshot);
|
|
5292
|
+
if (emitV1Snapshot || emitV2Snapshot) {
|
|
5293
|
+
this.seq++;
|
|
5294
|
+
if (emitV1Snapshot) {
|
|
5295
|
+
this.callbacks.onTurnsSnapshot(
|
|
5296
|
+
boundMarkdownTurnsForWire(this.turns.slice(-MAX_REPLAY_TURNS)),
|
|
5297
|
+
this.seq
|
|
5298
|
+
);
|
|
5299
|
+
}
|
|
5300
|
+
if (emitV2Snapshot) {
|
|
5301
|
+
this.callbacks.onSnapshot(this.snapshot, this.seq);
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
3824
5304
|
if (this.offset < savedOffset) {
|
|
3825
5305
|
this.offset = savedOffset;
|
|
3826
5306
|
}
|
|
3827
5307
|
}
|
|
5308
|
+
/** Await the current historical replay. Small replays resolve immediately. */
|
|
5309
|
+
waitForReplay() {
|
|
5310
|
+
return this.replayPromise;
|
|
5311
|
+
}
|
|
5312
|
+
async replayLineHistoryAsync(replayEnd, replayInode, generation, wantsV1, wantsV2) {
|
|
5313
|
+
this.offset = replayEnd;
|
|
5314
|
+
this.partialLine = "";
|
|
5315
|
+
this.discardingOversizedLine = false;
|
|
5316
|
+
this.decoder = new import_string_decoder.StringDecoder("utf8");
|
|
5317
|
+
this.turns = [];
|
|
5318
|
+
this.snapshot = createEmptySnapshot(this.snapshot.sessionId);
|
|
5319
|
+
let carry = "";
|
|
5320
|
+
let discardingOversizedLine = false;
|
|
5321
|
+
let recordsSinceYield = 0;
|
|
5322
|
+
let sourceBytesSinceYield = 0;
|
|
5323
|
+
let pendingReplayOps = [];
|
|
5324
|
+
const stream = fs2.createReadStream(this.filePath, {
|
|
5325
|
+
start: 0,
|
|
5326
|
+
end: Math.max(0, replayEnd - 1),
|
|
5327
|
+
encoding: "utf8",
|
|
5328
|
+
highWaterMark: READ_CHUNK_BYTES
|
|
5329
|
+
});
|
|
5330
|
+
for await (const rawChunk of stream) {
|
|
5331
|
+
if (generation !== this.replayGeneration || this.stopped) {
|
|
5332
|
+
stream.destroy();
|
|
5333
|
+
return;
|
|
5334
|
+
}
|
|
5335
|
+
let chunk = String(rawChunk);
|
|
5336
|
+
if (discardingOversizedLine) {
|
|
5337
|
+
const recordEnd = chunk.indexOf("\n");
|
|
5338
|
+
if (recordEnd < 0) continue;
|
|
5339
|
+
discardingOversizedLine = false;
|
|
5340
|
+
chunk = chunk.slice(recordEnd + 1);
|
|
5341
|
+
}
|
|
5342
|
+
const combined = carry + chunk;
|
|
5343
|
+
let lineStart = 0;
|
|
5344
|
+
let lineEnd = combined.indexOf("\n", lineStart);
|
|
5345
|
+
while (lineEnd >= 0) {
|
|
5346
|
+
const line = combined.slice(lineStart, lineEnd);
|
|
5347
|
+
lineStart = lineEnd + 1;
|
|
5348
|
+
const lineBytes = Buffer.byteLength(line, "utf8");
|
|
5349
|
+
if (lineBytes > MAX_JSONL_RECORD_BYTES) {
|
|
5350
|
+
this.reportOversizedRecord();
|
|
5351
|
+
} else {
|
|
5352
|
+
const trimmed = line.trim();
|
|
5353
|
+
if (trimmed) {
|
|
5354
|
+
this.parseHistoricalLine(
|
|
5355
|
+
trimmed,
|
|
5356
|
+
wantsV1,
|
|
5357
|
+
wantsV2,
|
|
5358
|
+
pendingReplayOps
|
|
5359
|
+
);
|
|
5360
|
+
}
|
|
5361
|
+
}
|
|
5362
|
+
recordsSinceYield++;
|
|
5363
|
+
sourceBytesSinceYield += lineBytes;
|
|
5364
|
+
if (recordsSinceYield >= ASYNC_REPLAY_YIELD_RECORDS || sourceBytesSinceYield >= MAX_READ_BYTES_PER_PASS) {
|
|
5365
|
+
recordsSinceYield = 0;
|
|
5366
|
+
sourceBytesSinceYield = 0;
|
|
5367
|
+
if (pendingReplayOps.length > 0) {
|
|
5368
|
+
this.snapshot = applyOps(this.snapshot, pendingReplayOps);
|
|
5369
|
+
pendingReplayOps = [];
|
|
5370
|
+
}
|
|
5371
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
5372
|
+
if (generation !== this.replayGeneration || this.stopped) {
|
|
5373
|
+
stream.destroy();
|
|
5374
|
+
return;
|
|
5375
|
+
}
|
|
5376
|
+
}
|
|
5377
|
+
lineEnd = combined.indexOf("\n", lineStart);
|
|
5378
|
+
}
|
|
5379
|
+
carry = combined.slice(lineStart);
|
|
5380
|
+
if (Buffer.byteLength(carry, "utf8") > MAX_JSONL_RECORD_BYTES) {
|
|
5381
|
+
this.reportOversizedRecord();
|
|
5382
|
+
carry = "";
|
|
5383
|
+
discardingOversizedLine = true;
|
|
5384
|
+
}
|
|
5385
|
+
}
|
|
5386
|
+
if (generation !== this.replayGeneration || this.stopped) return;
|
|
5387
|
+
try {
|
|
5388
|
+
const currentStat = fs2.statSync(this.filePath);
|
|
5389
|
+
if (currentStat.ino !== replayInode || currentStat.size < replayEnd) {
|
|
5390
|
+
this.restartAfterRotation(currentStat);
|
|
5391
|
+
return;
|
|
5392
|
+
}
|
|
5393
|
+
} catch {
|
|
5394
|
+
return;
|
|
5395
|
+
}
|
|
5396
|
+
if (pendingReplayOps.length > 0) {
|
|
5397
|
+
this.snapshot = applyOps(this.snapshot, pendingReplayOps);
|
|
5398
|
+
}
|
|
5399
|
+
this.partialLine = carry;
|
|
5400
|
+
this.discardingOversizedLine = discardingOversizedLine;
|
|
5401
|
+
this.seq++;
|
|
5402
|
+
if (wantsV1 && this.callbacks.onTurnsSnapshot) {
|
|
5403
|
+
this.callbacks.onTurnsSnapshot(
|
|
5404
|
+
boundMarkdownTurnsForWire(this.turns.slice(-MAX_REPLAY_TURNS)),
|
|
5405
|
+
this.seq
|
|
5406
|
+
);
|
|
5407
|
+
}
|
|
5408
|
+
if (wantsV2 && this.callbacks.onSnapshot) {
|
|
5409
|
+
this.snapshot = trimSnapshot(this.snapshot);
|
|
5410
|
+
this.callbacks.onSnapshot(this.snapshot, this.seq);
|
|
5411
|
+
}
|
|
5412
|
+
}
|
|
5413
|
+
parseHistoricalLine(line, wantsV1, wantsV2, pendingReplayOps) {
|
|
5414
|
+
if (wantsV2 && this.parserV2) {
|
|
5415
|
+
try {
|
|
5416
|
+
const ops = this.parserV2.parseLine(line);
|
|
5417
|
+
if (ops.length > 0) pendingReplayOps.push(...ops);
|
|
5418
|
+
} catch (error) {
|
|
5419
|
+
this.reportParserFailure(this.parserV2.name, "line", error);
|
|
5420
|
+
}
|
|
5421
|
+
}
|
|
5422
|
+
if (!wantsV1) return;
|
|
5423
|
+
try {
|
|
5424
|
+
const turn = this.parser.parseLine(line);
|
|
5425
|
+
if (!turn) return;
|
|
5426
|
+
this.turns.push(boundMarkdownTurnForWire({
|
|
5427
|
+
turnId: turn.turnId,
|
|
5428
|
+
role: turn.role,
|
|
5429
|
+
content: turn.content,
|
|
5430
|
+
tools: turn.tools,
|
|
5431
|
+
timestamp: turn.timestamp
|
|
5432
|
+
}));
|
|
5433
|
+
if (this.turns.length > MAX_REPLAY_TURNS * 2) {
|
|
5434
|
+
this.turns = this.turns.slice(-MAX_REPLAY_TURNS);
|
|
5435
|
+
}
|
|
5436
|
+
} catch (error) {
|
|
5437
|
+
this.reportParserFailure(this.parser.name, "line", error);
|
|
5438
|
+
}
|
|
5439
|
+
}
|
|
3828
5440
|
/**
|
|
3829
5441
|
* If the V2 parser supports parseDocument (e.g. Codex), rebuild the V2
|
|
3830
5442
|
* snapshot from the full file contents and notify subscribers. This is the
|
|
@@ -3832,24 +5444,31 @@ var TranscriptWatcher = class {
|
|
|
3832
5444
|
* unblocks blank-on-attach for agents whose V1 parser is line-based but
|
|
3833
5445
|
* whose V2 parser can replay the whole document. See #473.
|
|
3834
5446
|
*/
|
|
3835
|
-
rebuildV2SnapshotFromDocument() {
|
|
3836
|
-
if (!this.parserV2?.parseDocument
|
|
3837
|
-
let
|
|
5447
|
+
rebuildV2SnapshotFromDocument(content) {
|
|
5448
|
+
if (!this.parserV2?.parseDocument) return false;
|
|
5449
|
+
let ops;
|
|
3838
5450
|
try {
|
|
3839
|
-
|
|
3840
|
-
} catch {
|
|
3841
|
-
|
|
5451
|
+
ops = this.parserV2.parseDocument(content);
|
|
5452
|
+
} catch (error) {
|
|
5453
|
+
this.reportParserFailure(this.parserV2.name, "document", error);
|
|
5454
|
+
return false;
|
|
3842
5455
|
}
|
|
3843
|
-
const ops = this.parserV2.parseDocument(content);
|
|
3844
5456
|
if (ops.length === 0 && this.snapshot.turnOrder.length > 0) {
|
|
3845
|
-
return;
|
|
5457
|
+
return false;
|
|
3846
5458
|
}
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
5459
|
+
try {
|
|
5460
|
+
let nextSnapshot = createEmptySnapshot(this.snapshot.sessionId);
|
|
5461
|
+
if (ops.length > 0) {
|
|
5462
|
+
for (const batch of boundTranscriptOpsForWire(ops)) {
|
|
5463
|
+
nextSnapshot = applyOps(nextSnapshot, batch);
|
|
5464
|
+
}
|
|
5465
|
+
}
|
|
5466
|
+
this.snapshot = nextSnapshot;
|
|
5467
|
+
return true;
|
|
5468
|
+
} catch (error) {
|
|
5469
|
+
this.reportParserFailure(this.parserV2.name, "document", error);
|
|
5470
|
+
return false;
|
|
3850
5471
|
}
|
|
3851
|
-
this.snapshot = trimSnapshot(nextSnapshot);
|
|
3852
|
-
this.callbacks.onSnapshot(this.snapshot, this.seq);
|
|
3853
5472
|
}
|
|
3854
5473
|
startWatching() {
|
|
3855
5474
|
try {
|
|
@@ -3882,17 +5501,54 @@ var TranscriptWatcher = class {
|
|
|
3882
5501
|
try {
|
|
3883
5502
|
const stat = fs2.statSync(this.filePath);
|
|
3884
5503
|
if (stat.ino !== this.inode) {
|
|
3885
|
-
this.
|
|
3886
|
-
|
|
3887
|
-
|
|
5504
|
+
if (this.parser.parseDocument) {
|
|
5505
|
+
this.resetReadCursor(stat);
|
|
5506
|
+
} else {
|
|
5507
|
+
this.restartAfterRotation(stat);
|
|
5508
|
+
}
|
|
3888
5509
|
} else if (stat.size < this.offset) {
|
|
3889
|
-
this.
|
|
3890
|
-
|
|
5510
|
+
if (this.parser.parseDocument) {
|
|
5511
|
+
this.resetReadCursor(stat);
|
|
5512
|
+
} else {
|
|
5513
|
+
this.restartAfterRotation(stat);
|
|
5514
|
+
}
|
|
3891
5515
|
}
|
|
3892
5516
|
} catch {
|
|
3893
5517
|
}
|
|
3894
5518
|
}
|
|
5519
|
+
resetReadCursor(stat) {
|
|
5520
|
+
this.flushPendingOps();
|
|
5521
|
+
this.inode = stat.ino;
|
|
5522
|
+
this.offset = 0;
|
|
5523
|
+
this.partialLine = "";
|
|
5524
|
+
this.discardingOversizedLine = false;
|
|
5525
|
+
this.decoder = new import_string_decoder.StringDecoder("utf8");
|
|
5526
|
+
}
|
|
5527
|
+
restartAfterRotation(stat) {
|
|
5528
|
+
this.flushPendingOps();
|
|
5529
|
+
this.replayGeneration++;
|
|
5530
|
+
this.replayInProgress = false;
|
|
5531
|
+
this.replayReadPending = false;
|
|
5532
|
+
this.inode = stat.ino;
|
|
5533
|
+
this.offset = stat.size;
|
|
5534
|
+
this.partialLine = "";
|
|
5535
|
+
this.discardingOversizedLine = false;
|
|
5536
|
+
this.decoder = new import_string_decoder.StringDecoder("utf8");
|
|
5537
|
+
this.turns = [];
|
|
5538
|
+
this.snapshot = createEmptySnapshot(this.snapshot.sessionId);
|
|
5539
|
+
if (this.callbacks.shouldProcessV1?.() !== false) {
|
|
5540
|
+
this.callbacks.onTurnsSnapshot?.([], 0);
|
|
5541
|
+
}
|
|
5542
|
+
if ((this.callbacks.shouldProcessV2?.() ?? Boolean(this.callbacks.onSnapshot)) && this.callbacks.onSnapshot) {
|
|
5543
|
+
this.callbacks.onSnapshot(this.snapshot, 0, true);
|
|
5544
|
+
}
|
|
5545
|
+
this.replayFromStart();
|
|
5546
|
+
}
|
|
3895
5547
|
readNewData() {
|
|
5548
|
+
if (this.replayInProgress) {
|
|
5549
|
+
this.replayReadPending = true;
|
|
5550
|
+
return;
|
|
5551
|
+
}
|
|
3896
5552
|
if (this.parser.parseDocument) {
|
|
3897
5553
|
this.readDocument(false);
|
|
3898
5554
|
return;
|
|
@@ -3906,17 +5562,36 @@ var TranscriptWatcher = class {
|
|
|
3906
5562
|
try {
|
|
3907
5563
|
const stat = fs2.fstatSync(fd);
|
|
3908
5564
|
if (stat.size <= this.offset) return;
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
5565
|
+
let remaining = Math.min(
|
|
5566
|
+
stat.size - this.offset,
|
|
5567
|
+
MAX_READ_BYTES_PER_PASS
|
|
5568
|
+
);
|
|
5569
|
+
while (remaining > 0) {
|
|
5570
|
+
const readSize = Math.min(remaining, READ_CHUNK_BYTES);
|
|
5571
|
+
const buffer = Buffer.allocUnsafe(readSize);
|
|
5572
|
+
const bytesRead = fs2.readSync(fd, buffer, 0, readSize, this.offset);
|
|
5573
|
+
if (bytesRead === 0) break;
|
|
5574
|
+
this.offset += bytesRead;
|
|
5575
|
+
remaining -= bytesRead;
|
|
5576
|
+
this.processChunk(this.decoder.write(buffer.subarray(0, bytesRead)));
|
|
5577
|
+
}
|
|
5578
|
+
if (this.offset < stat.size) {
|
|
5579
|
+
this.scheduleImmediateRead();
|
|
5580
|
+
}
|
|
3916
5581
|
} finally {
|
|
3917
5582
|
fs2.closeSync(fd);
|
|
3918
5583
|
}
|
|
3919
5584
|
}
|
|
5585
|
+
scheduleImmediateRead() {
|
|
5586
|
+
if (this.immediateReadTimer || this.stopped) return;
|
|
5587
|
+
this.immediateReadTimer = setTimeout(() => {
|
|
5588
|
+
this.immediateReadTimer = null;
|
|
5589
|
+
if (!this.stopped) this.readNewData();
|
|
5590
|
+
}, 0);
|
|
5591
|
+
if (typeof this.immediateReadTimer.unref === "function") {
|
|
5592
|
+
this.immediateReadTimer.unref();
|
|
5593
|
+
}
|
|
5594
|
+
}
|
|
3920
5595
|
readDocument(force) {
|
|
3921
5596
|
let stat;
|
|
3922
5597
|
try {
|
|
@@ -3933,68 +5608,142 @@ var TranscriptWatcher = class {
|
|
|
3933
5608
|
return;
|
|
3934
5609
|
}
|
|
3935
5610
|
this.documentFingerprint = fingerprint;
|
|
3936
|
-
|
|
3937
|
-
|
|
5611
|
+
let parsedTurns;
|
|
5612
|
+
try {
|
|
5613
|
+
parsedTurns = this.parser.parseDocument?.(content) ?? [];
|
|
5614
|
+
} catch (error) {
|
|
5615
|
+
this.reportParserFailure(this.parser.name, "document", error);
|
|
5616
|
+
return;
|
|
5617
|
+
}
|
|
5618
|
+
let v2Ops;
|
|
5619
|
+
try {
|
|
5620
|
+
v2Ops = this.parserV2?.parseDocument?.(content) ?? [];
|
|
5621
|
+
} catch (error) {
|
|
5622
|
+
this.reportParserFailure(
|
|
5623
|
+
this.parserV2?.name ?? "transcript-v2",
|
|
5624
|
+
"document",
|
|
5625
|
+
error
|
|
5626
|
+
);
|
|
5627
|
+
return;
|
|
5628
|
+
}
|
|
3938
5629
|
const parsedNothing = parsedTurns.length === 0 && v2Ops.length === 0;
|
|
3939
5630
|
const haveContent = this.turns.length > 0 || this.snapshot.turnOrder.length > 0;
|
|
3940
5631
|
if (parsedNothing && haveContent) {
|
|
3941
5632
|
return;
|
|
3942
5633
|
}
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
5634
|
+
try {
|
|
5635
|
+
this.flushPendingOps();
|
|
5636
|
+
this.seq++;
|
|
5637
|
+
this.turns = parsedTurns.slice(-MAX_REPLAY_TURNS).map((turn) => boundMarkdownTurnForWire({
|
|
5638
|
+
turnId: turn.turnId,
|
|
5639
|
+
role: turn.role,
|
|
5640
|
+
content: turn.content,
|
|
5641
|
+
tools: turn.tools,
|
|
5642
|
+
timestamp: turn.timestamp
|
|
5643
|
+
}));
|
|
5644
|
+
this.callbacks.onTurnsSnapshot?.(
|
|
5645
|
+
boundMarkdownTurnsForWire(this.turns),
|
|
5646
|
+
this.seq
|
|
5647
|
+
);
|
|
5648
|
+
if (this.parserV2?.parseDocument && this.callbacks.onSnapshot) {
|
|
5649
|
+
let nextSnapshot = createEmptySnapshot(this.snapshot.sessionId);
|
|
5650
|
+
if (v2Ops.length > 0) {
|
|
5651
|
+
for (const batch of boundTranscriptOpsForWire(v2Ops)) {
|
|
5652
|
+
nextSnapshot = applyOps(nextSnapshot, batch);
|
|
5653
|
+
}
|
|
5654
|
+
}
|
|
5655
|
+
this.snapshot = nextSnapshot;
|
|
5656
|
+
this.callbacks.onSnapshot(this.snapshot, this.seq);
|
|
3956
5657
|
}
|
|
3957
|
-
|
|
3958
|
-
this.
|
|
5658
|
+
} catch (error) {
|
|
5659
|
+
this.reportParserFailure(
|
|
5660
|
+
this.parserV2?.name ?? this.parser.name,
|
|
5661
|
+
"document",
|
|
5662
|
+
error
|
|
5663
|
+
);
|
|
3959
5664
|
}
|
|
3960
5665
|
}
|
|
3961
5666
|
processChunk(chunk) {
|
|
5667
|
+
if (this.discardingOversizedLine) {
|
|
5668
|
+
const recordEnd = chunk.indexOf("\n");
|
|
5669
|
+
if (recordEnd < 0) return;
|
|
5670
|
+
this.discardingOversizedLine = false;
|
|
5671
|
+
chunk = chunk.slice(recordEnd + 1);
|
|
5672
|
+
}
|
|
3962
5673
|
const combined = this.partialLine + chunk;
|
|
3963
5674
|
const lines = combined.split("\n");
|
|
3964
5675
|
this.partialLine = lines.pop() ?? "";
|
|
3965
|
-
if (this.partialLine
|
|
3966
|
-
this.
|
|
5676
|
+
if (Buffer.byteLength(this.partialLine, "utf8") > MAX_JSONL_RECORD_BYTES) {
|
|
5677
|
+
this.reportOversizedRecord();
|
|
3967
5678
|
this.partialLine = "";
|
|
5679
|
+
this.discardingOversizedLine = true;
|
|
3968
5680
|
}
|
|
3969
5681
|
for (const line of lines) {
|
|
5682
|
+
if (Buffer.byteLength(line, "utf8") > MAX_JSONL_RECORD_BYTES) {
|
|
5683
|
+
this.reportOversizedRecord();
|
|
5684
|
+
continue;
|
|
5685
|
+
}
|
|
3970
5686
|
const trimmed = line.trim();
|
|
3971
5687
|
if (!trimmed) continue;
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
this.seq++;
|
|
3985
|
-
}
|
|
3986
|
-
this.turns.push({
|
|
3987
|
-
turnId: turn.turnId,
|
|
3988
|
-
role: turn.role,
|
|
3989
|
-
content: turn.content,
|
|
3990
|
-
tools: turn.tools,
|
|
3991
|
-
timestamp: turn.timestamp
|
|
3992
|
-
});
|
|
3993
|
-
if (this.turns.length > MAX_REPLAY_TURNS * 2) {
|
|
3994
|
-
this.turns = this.turns.slice(-MAX_REPLAY_TURNS);
|
|
5688
|
+
let v2Queued = false;
|
|
5689
|
+
const deferV2ToDocumentReplay = this.replaying && this.suppressReplayV2Deltas;
|
|
5690
|
+
const shouldProcessV2 = this.callbacks.shouldProcessV2?.() ?? Boolean(this.callbacks.onOps || this.callbacks.onSnapshot);
|
|
5691
|
+
if (!deferV2ToDocumentReplay && this.parserV2 && shouldProcessV2 && (this.callbacks.onOps || this.callbacks.onSnapshot)) {
|
|
5692
|
+
try {
|
|
5693
|
+
const ops = this.parserV2.parseLine(trimmed);
|
|
5694
|
+
if (ops.length > 0) {
|
|
5695
|
+
this.queueV2Ops(ops);
|
|
5696
|
+
v2Queued = true;
|
|
5697
|
+
}
|
|
5698
|
+
} catch (error) {
|
|
5699
|
+
this.reportParserFailure(this.parserV2.name, "line", error);
|
|
3995
5700
|
}
|
|
3996
|
-
this.callbacks.onTurn(turn, this.seq);
|
|
3997
5701
|
}
|
|
5702
|
+
if (this.callbacks.shouldProcessV1?.() === false) continue;
|
|
5703
|
+
try {
|
|
5704
|
+
const turn = this.parser.parseLine(trimmed);
|
|
5705
|
+
if (turn) {
|
|
5706
|
+
const emitV1Delta = !this.replaying || !this.suppressReplayV1Deltas;
|
|
5707
|
+
if (!v2Queued && emitV1Delta) {
|
|
5708
|
+
this.flushPendingOps();
|
|
5709
|
+
this.seq++;
|
|
5710
|
+
}
|
|
5711
|
+
const boundedTurn = boundMarkdownTurnForWire({
|
|
5712
|
+
turnId: turn.turnId,
|
|
5713
|
+
role: turn.role,
|
|
5714
|
+
content: turn.content,
|
|
5715
|
+
tools: turn.tools,
|
|
5716
|
+
timestamp: turn.timestamp
|
|
5717
|
+
});
|
|
5718
|
+
this.turns.push(boundedTurn);
|
|
5719
|
+
if (this.turns.length > MAX_REPLAY_TURNS * 2) {
|
|
5720
|
+
this.turns = this.turns.slice(-MAX_REPLAY_TURNS);
|
|
5721
|
+
}
|
|
5722
|
+
if (emitV1Delta) {
|
|
5723
|
+
this.callbacks.onTurn(boundedTurn, this.seq);
|
|
5724
|
+
}
|
|
5725
|
+
}
|
|
5726
|
+
} catch (error) {
|
|
5727
|
+
this.reportParserFailure(this.parser.name, "line", error);
|
|
5728
|
+
}
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5731
|
+
reportOversizedRecord() {
|
|
5732
|
+
const message = `Transcript JSONL record exceeded ${MAX_JSONL_RECORD_BYTES} bytes; record omitted and streaming resumed at the next line`;
|
|
5733
|
+
try {
|
|
5734
|
+
if (this.callbacks.onError) {
|
|
5735
|
+
this.callbacks.onError(message);
|
|
5736
|
+
} else {
|
|
5737
|
+
log7.warn("oversized transcript record omitted", {
|
|
5738
|
+
filePath: this.filePath,
|
|
5739
|
+
maxBytes: MAX_JSONL_RECORD_BYTES
|
|
5740
|
+
});
|
|
5741
|
+
}
|
|
5742
|
+
} catch (error) {
|
|
5743
|
+
log7.warn("oversized transcript error callback threw", {
|
|
5744
|
+
filePath: this.filePath,
|
|
5745
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5746
|
+
});
|
|
3998
5747
|
}
|
|
3999
5748
|
}
|
|
4000
5749
|
};
|
|
@@ -4011,7 +5760,11 @@ function getParser(agentType) {
|
|
|
4011
5760
|
}
|
|
4012
5761
|
var TranscriptWatcherManager = class {
|
|
4013
5762
|
active = /* @__PURE__ */ new Map();
|
|
4014
|
-
|
|
5763
|
+
/** One staggered round-robin scheduler replaces synchronized per-session timers. */
|
|
5764
|
+
rediscoverySessions = /* @__PURE__ */ new Map();
|
|
5765
|
+
rediscoveryTimer;
|
|
5766
|
+
rediscoveryCursor = 0;
|
|
5767
|
+
rediscoveryIndex;
|
|
4015
5768
|
/** #542: sessions awaiting a markdown snapshot re-emit after a frame drop. */
|
|
4016
5769
|
pendingMarkdownResync = /* @__PURE__ */ new Set();
|
|
4017
5770
|
markdownResyncTimer;
|
|
@@ -4019,6 +5772,10 @@ var TranscriptWatcherManager = class {
|
|
|
4019
5772
|
enableRetryTimers = /* @__PURE__ */ new Map();
|
|
4020
5773
|
/** Index into ENABLE_RETRY_DELAYS_MS for the next scheduled attempt. */
|
|
4021
5774
|
enableRetryAttempts = /* @__PURE__ */ new Map();
|
|
5775
|
+
/** Desired subscriptions outlive transient discovery failures. Keeping this
|
|
5776
|
+
* per viewer prevents one pane's disable from cancelling another pane's
|
|
5777
|
+
* pending retry and lets a replacement watcher restore every subscriber. */
|
|
5778
|
+
desiredSubscriptions = /* @__PURE__ */ new Map();
|
|
4022
5779
|
sendFn;
|
|
4023
5780
|
sendSnapshotFn;
|
|
4024
5781
|
sendOpsFn;
|
|
@@ -4032,16 +5789,33 @@ var TranscriptWatcherManager = class {
|
|
|
4032
5789
|
this.onEnableRetrySuccess = onEnableRetrySuccess;
|
|
4033
5790
|
}
|
|
4034
5791
|
/** Enable markdown streaming for a session. */
|
|
4035
|
-
enableMarkdown(sessionId, agentType) {
|
|
5792
|
+
enableMarkdown(sessionId, agentType, viewerId, schemaVersion) {
|
|
5793
|
+
const subscriberKey = viewerId || "__legacy_anonymous__";
|
|
5794
|
+
let desired = this.desiredSubscriptions.get(sessionId);
|
|
5795
|
+
if (!desired) {
|
|
5796
|
+
desired = /* @__PURE__ */ new Map();
|
|
5797
|
+
this.desiredSubscriptions.set(sessionId, desired);
|
|
5798
|
+
}
|
|
5799
|
+
desired.set(subscriberKey, {
|
|
5800
|
+
agentType,
|
|
5801
|
+
schemaVersion: schemaVersion === 2 ? 2 : schemaVersion === 1 ? 1 : 0
|
|
5802
|
+
});
|
|
5803
|
+
if (!this.enableRetryTimers.has(sessionId) && !this.enableRetryAttempts.has(sessionId)) {
|
|
5804
|
+
this.enableRetryAttempts.set(sessionId, 0);
|
|
5805
|
+
}
|
|
4036
5806
|
return this.enableMarkdownInternal(
|
|
4037
5807
|
sessionId,
|
|
4038
5808
|
agentType,
|
|
4039
5809
|
/* isRetry */
|
|
4040
|
-
false
|
|
5810
|
+
false,
|
|
5811
|
+
viewerId,
|
|
5812
|
+
schemaVersion
|
|
4041
5813
|
);
|
|
4042
5814
|
}
|
|
4043
|
-
enableMarkdownInternal(sessionId, agentType, isRetry) {
|
|
5815
|
+
enableMarkdownInternal(sessionId, agentType, isRetry, viewerId, schemaVersion) {
|
|
4044
5816
|
const requestedAgentType = agentType ?? "";
|
|
5817
|
+
const subscriberKey = viewerId || "__legacy_anonymous__";
|
|
5818
|
+
const requestedSchema = schemaVersion === 2 ? 2 : schemaVersion === 1 ? 1 : 0;
|
|
4045
5819
|
const lockedSource = getSessionSourceLock(sessionId);
|
|
4046
5820
|
if (lockedSource && lockedSource !== "file-watcher") {
|
|
4047
5821
|
log7.info("native-stream consumer owns session; skipping file watch", {
|
|
@@ -4049,11 +5823,22 @@ var TranscriptWatcherManager = class {
|
|
|
4049
5823
|
agentType,
|
|
4050
5824
|
lockedSource
|
|
4051
5825
|
});
|
|
4052
|
-
this.
|
|
5826
|
+
this.removeDesiredSubscription(sessionId, subscriberKey);
|
|
5827
|
+
this.cancelEnableRetryIfUnused(sessionId);
|
|
5828
|
+
return { ok: false, reason: "unsupported_agent" };
|
|
5829
|
+
}
|
|
5830
|
+
const parser = getParser(agentType);
|
|
5831
|
+
if (!parser) {
|
|
5832
|
+
log7.info("no transcript parser for agent type", { agentType });
|
|
5833
|
+
this.removeDesiredSubscription(sessionId, subscriberKey);
|
|
5834
|
+
this.cancelEnableRetryIfUnused(sessionId);
|
|
4053
5835
|
return { ok: false, reason: "unsupported_agent" };
|
|
4054
5836
|
}
|
|
4055
5837
|
const existing = this.active.get(sessionId);
|
|
4056
5838
|
let replacingExisting = false;
|
|
5839
|
+
let replacementSubscribers;
|
|
5840
|
+
let existingToReplace;
|
|
5841
|
+
let replacementFilePath;
|
|
4057
5842
|
if (existing) {
|
|
4058
5843
|
const newPath = discoverTranscriptFileDetailed(
|
|
4059
5844
|
sessionId,
|
|
@@ -4064,34 +5849,31 @@ var TranscriptWatcherManager = class {
|
|
|
4064
5849
|
const pathChanged = Boolean(newPath && newPath !== existing.filePath);
|
|
4065
5850
|
const existingPathGone = !fs2.existsSync(existing.filePath);
|
|
4066
5851
|
if (!agentChanged && !pathChanged && !existingPathGone) {
|
|
4067
|
-
existing.
|
|
5852
|
+
if (!existing.subscribers.has(subscriberKey)) {
|
|
5853
|
+
existing.watcher.addSubscriber();
|
|
5854
|
+
}
|
|
5855
|
+
existing.subscribers.set(subscriberKey, requestedSchema);
|
|
4068
5856
|
this.cancelEnableRetry(sessionId);
|
|
4069
5857
|
return { ok: true };
|
|
4070
5858
|
}
|
|
4071
|
-
existing.
|
|
4072
|
-
|
|
4073
|
-
this.stopRediscovery(sessionId);
|
|
4074
|
-
replacingExisting = true;
|
|
5859
|
+
replacementSubscribers = new Map(existing.subscribers);
|
|
5860
|
+
replacementSubscribers.set(subscriberKey, requestedSchema);
|
|
4075
5861
|
if (!newPath) {
|
|
4076
5862
|
log7.info("no replacement transcript file found", { sessionId, agentType });
|
|
4077
|
-
this.armEnableRetry(sessionId
|
|
5863
|
+
this.armEnableRetry(sessionId);
|
|
4078
5864
|
return { ok: false, reason: "no_transcript" };
|
|
4079
5865
|
}
|
|
5866
|
+
existingToReplace = existing;
|
|
5867
|
+
replacementFilePath = newPath;
|
|
4080
5868
|
}
|
|
4081
|
-
const
|
|
4082
|
-
if (!parser) {
|
|
4083
|
-
log7.info("no transcript parser for agent type", { agentType });
|
|
4084
|
-
this.cancelEnableRetry(sessionId);
|
|
4085
|
-
return { ok: false, reason: "unsupported_agent" };
|
|
4086
|
-
}
|
|
4087
|
-
const filePath = discoverTranscriptFileDetailed(
|
|
5869
|
+
const filePath = replacementFilePath ?? discoverTranscriptFileDetailed(
|
|
4088
5870
|
sessionId,
|
|
4089
5871
|
agentType,
|
|
4090
5872
|
this.ownedPathsExcept(sessionId)
|
|
4091
5873
|
).path;
|
|
4092
5874
|
if (!filePath) {
|
|
4093
5875
|
log7.info("no transcript file found", { sessionId });
|
|
4094
|
-
this.armEnableRetry(sessionId
|
|
5876
|
+
this.armEnableRetry(sessionId);
|
|
4095
5877
|
return { ok: false, reason: "no_transcript" };
|
|
4096
5878
|
}
|
|
4097
5879
|
log7.info("watching transcript", { path: filePath, parser: parser.name });
|
|
@@ -4101,17 +5883,27 @@ var TranscriptWatcherManager = class {
|
|
|
4101
5883
|
parser,
|
|
4102
5884
|
{
|
|
4103
5885
|
onTurn: (turn, seq) => {
|
|
4104
|
-
this.
|
|
5886
|
+
if (this.hasLegacySubscribers(sessionId)) {
|
|
5887
|
+
this.sendFn(sessionId, turn, seq);
|
|
5888
|
+
}
|
|
4105
5889
|
},
|
|
4106
5890
|
onOps: this.sendOpsFn ? (ops, seq) => {
|
|
4107
|
-
this.
|
|
5891
|
+
if (this.hasV2Subscribers(sessionId)) {
|
|
5892
|
+
this.sendOpsFn(sessionId, ops, seq);
|
|
5893
|
+
}
|
|
4108
5894
|
} : void 0,
|
|
4109
5895
|
onTurnsSnapshot: (turns, seq) => {
|
|
4110
|
-
this.
|
|
5896
|
+
if (this.hasLegacySubscribers(sessionId)) {
|
|
5897
|
+
this.sendSnapshotFn(sessionId, turns, seq);
|
|
5898
|
+
}
|
|
4111
5899
|
},
|
|
4112
|
-
onSnapshot: this.sendV2SnapshotFn ? (snapshot, seq) => {
|
|
4113
|
-
this.
|
|
5900
|
+
onSnapshot: this.sendV2SnapshotFn ? (snapshot, seq, reset) => {
|
|
5901
|
+
if (this.hasV2Subscribers(sessionId)) {
|
|
5902
|
+
this.sendV2SnapshotFn(sessionId, snapshot, seq, reset);
|
|
5903
|
+
}
|
|
4114
5904
|
} : void 0,
|
|
5905
|
+
shouldProcessV1: () => this.hasLegacySubscribers(sessionId),
|
|
5906
|
+
shouldProcessV2: () => this.hasV2Subscribers(sessionId),
|
|
4115
5907
|
onError: (error) => {
|
|
4116
5908
|
log7.warn("transcript watcher error", { sessionId, error: String(error) });
|
|
4117
5909
|
}
|
|
@@ -4121,13 +5913,32 @@ var TranscriptWatcherManager = class {
|
|
|
4121
5913
|
);
|
|
4122
5914
|
if (!watcher.start()) {
|
|
4123
5915
|
log7.warn("failed to start watching", { path: filePath });
|
|
4124
|
-
this.armEnableRetry(sessionId
|
|
5916
|
+
this.armEnableRetry(sessionId);
|
|
4125
5917
|
return { ok: false, reason: "watch_failed" };
|
|
4126
5918
|
}
|
|
5919
|
+
const subscribers = replacementSubscribers ?? /* @__PURE__ */ new Map();
|
|
5920
|
+
for (const [key, intent] of this.desiredSubscriptions.get(sessionId) ?? []) {
|
|
5921
|
+
subscribers.set(key, intent.schemaVersion);
|
|
5922
|
+
}
|
|
5923
|
+
if (subscribers.size === 0) {
|
|
5924
|
+
subscribers.set(subscriberKey, requestedSchema);
|
|
5925
|
+
}
|
|
5926
|
+
for (let i = 1; i < subscribers.size; i++) {
|
|
5927
|
+
watcher.addSubscriber();
|
|
5928
|
+
}
|
|
5929
|
+
if (existingToReplace) {
|
|
5930
|
+
existingToReplace.watcher.stop();
|
|
5931
|
+
replacingExisting = true;
|
|
5932
|
+
}
|
|
5933
|
+
this.active.set(sessionId, {
|
|
5934
|
+
watcher,
|
|
5935
|
+
filePath,
|
|
5936
|
+
agentType: agentType ?? "",
|
|
5937
|
+
subscribers
|
|
5938
|
+
});
|
|
4127
5939
|
if (replacingExisting) {
|
|
4128
5940
|
this.clearTranscriptState(sessionId);
|
|
4129
5941
|
}
|
|
4130
|
-
this.active.set(sessionId, { watcher, filePath, agentType: agentType ?? "" });
|
|
4131
5942
|
watcher.replayFromStart();
|
|
4132
5943
|
this.startRediscovery(sessionId, agentType);
|
|
4133
5944
|
this.cancelEnableRetry(sessionId);
|
|
@@ -4148,24 +5959,39 @@ var TranscriptWatcherManager = class {
|
|
|
4148
5959
|
* loading placeholder, and a subsequent prefer-markdown from the portal
|
|
4149
5960
|
* (e.g. on reconnect or pane re-focus) will start a fresh attempt.
|
|
4150
5961
|
*/
|
|
4151
|
-
armEnableRetry(sessionId
|
|
5962
|
+
armEnableRetry(sessionId) {
|
|
5963
|
+
const desired = this.desiredSubscriptions.get(sessionId);
|
|
5964
|
+
if (!desired || desired.size === 0) {
|
|
5965
|
+
this.cancelEnableRetry(sessionId);
|
|
5966
|
+
return;
|
|
5967
|
+
}
|
|
4152
5968
|
const nextAttempt = this.enableRetryAttempts.get(sessionId) ?? 0;
|
|
4153
5969
|
if (nextAttempt >= ENABLE_RETRY_DELAYS_MS.length) {
|
|
4154
5970
|
this.cancelEnableRetry(sessionId);
|
|
4155
5971
|
return;
|
|
4156
5972
|
}
|
|
4157
5973
|
const existing = this.enableRetryTimers.get(sessionId);
|
|
4158
|
-
if (existing)
|
|
5974
|
+
if (existing) return;
|
|
4159
5975
|
const delay2 = ENABLE_RETRY_DELAYS_MS[nextAttempt];
|
|
4160
5976
|
const timer = setTimeout(() => {
|
|
4161
5977
|
this.enableRetryAttempts.set(sessionId, nextAttempt + 1);
|
|
4162
5978
|
this.enableRetryTimers.delete(sessionId);
|
|
5979
|
+
const currentDesired = this.desiredSubscriptions.get(sessionId);
|
|
5980
|
+
const next = currentDesired?.entries().next();
|
|
5981
|
+
if (!next || next.done) {
|
|
5982
|
+
this.cancelEnableRetry(sessionId);
|
|
5983
|
+
return;
|
|
5984
|
+
}
|
|
5985
|
+
const [subscriberKey, intent] = next.value;
|
|
5986
|
+
const viewerId = subscriberKey === "__legacy_anonymous__" ? void 0 : subscriberKey;
|
|
4163
5987
|
try {
|
|
4164
5988
|
this.enableMarkdownInternal(
|
|
4165
5989
|
sessionId,
|
|
4166
|
-
agentType,
|
|
5990
|
+
intent.agentType,
|
|
4167
5991
|
/* isRetry */
|
|
4168
|
-
true
|
|
5992
|
+
true,
|
|
5993
|
+
viewerId,
|
|
5994
|
+
intent.schemaVersion
|
|
4169
5995
|
);
|
|
4170
5996
|
} catch (err) {
|
|
4171
5997
|
log7.warn("enableMarkdown retry threw", { sessionId, err: String(err) });
|
|
@@ -4182,9 +6008,22 @@ var TranscriptWatcherManager = class {
|
|
|
4182
6008
|
}
|
|
4183
6009
|
this.enableRetryAttempts.delete(sessionId);
|
|
4184
6010
|
}
|
|
6011
|
+
removeDesiredSubscription(sessionId, subscriberKey) {
|
|
6012
|
+
const desired = this.desiredSubscriptions.get(sessionId);
|
|
6013
|
+
if (!desired) return;
|
|
6014
|
+
desired.delete(subscriberKey);
|
|
6015
|
+
if (desired.size === 0) this.desiredSubscriptions.delete(sessionId);
|
|
6016
|
+
}
|
|
6017
|
+
cancelEnableRetryIfUnused(sessionId) {
|
|
6018
|
+
if ((this.desiredSubscriptions.get(sessionId)?.size ?? 0) === 0) {
|
|
6019
|
+
this.cancelEnableRetry(sessionId);
|
|
6020
|
+
}
|
|
6021
|
+
}
|
|
4185
6022
|
clearTranscriptState(sessionId) {
|
|
4186
|
-
this.
|
|
4187
|
-
|
|
6023
|
+
if (this.hasLegacySubscribers(sessionId)) {
|
|
6024
|
+
this.sendSnapshotFn(sessionId, [], 0);
|
|
6025
|
+
}
|
|
6026
|
+
if (this.hasV2Subscribers(sessionId) && this.sendV2SnapshotFn) {
|
|
4188
6027
|
this.sendV2SnapshotFn(sessionId, createEmptySnapshot(sessionId), 0, true);
|
|
4189
6028
|
}
|
|
4190
6029
|
}
|
|
@@ -4204,15 +6043,52 @@ var TranscriptWatcherManager = class {
|
|
|
4204
6043
|
}
|
|
4205
6044
|
return owned;
|
|
4206
6045
|
}
|
|
6046
|
+
hasLegacySubscribers(sessionId) {
|
|
6047
|
+
const session = this.active.get(sessionId);
|
|
6048
|
+
if (!session) return true;
|
|
6049
|
+
for (const schemaVersion of session.subscribers.values()) {
|
|
6050
|
+
if (schemaVersion !== 2) return true;
|
|
6051
|
+
}
|
|
6052
|
+
return false;
|
|
6053
|
+
}
|
|
6054
|
+
hasV2Subscribers(sessionId) {
|
|
6055
|
+
const session = this.active.get(sessionId);
|
|
6056
|
+
if (!session) return false;
|
|
6057
|
+
for (const schemaVersion of session.subscribers.values()) {
|
|
6058
|
+
if (schemaVersion !== 1) return true;
|
|
6059
|
+
}
|
|
6060
|
+
return false;
|
|
6061
|
+
}
|
|
4207
6062
|
/** Disable markdown streaming for a session. */
|
|
4208
|
-
disableMarkdown(sessionId) {
|
|
4209
|
-
|
|
6063
|
+
disableMarkdown(sessionId, viewerId) {
|
|
6064
|
+
const subscriberKey = viewerId || "__legacy_anonymous__";
|
|
6065
|
+
const desired = this.desiredSubscriptions.get(sessionId);
|
|
6066
|
+
const stopWholeSession = !viewerId && !desired?.has(subscriberKey);
|
|
6067
|
+
if (stopWholeSession) {
|
|
6068
|
+
this.desiredSubscriptions.delete(sessionId);
|
|
6069
|
+
this.cancelEnableRetry(sessionId);
|
|
6070
|
+
} else {
|
|
6071
|
+
this.removeDesiredSubscription(sessionId, subscriberKey);
|
|
6072
|
+
this.cancelEnableRetryIfUnused(sessionId);
|
|
6073
|
+
}
|
|
4210
6074
|
const session = this.active.get(sessionId);
|
|
4211
6075
|
if (!session) return;
|
|
6076
|
+
if (!viewerId && !session.subscribers.has(subscriberKey)) {
|
|
6077
|
+
session.subscribers.clear();
|
|
6078
|
+
session.watcher.stop();
|
|
6079
|
+
this.active.delete(sessionId);
|
|
6080
|
+
this.stopRediscovery(sessionId);
|
|
6081
|
+
this.pendingMarkdownResync.delete(sessionId);
|
|
6082
|
+
log7.info("stopped watching transcript", { sessionId });
|
|
6083
|
+
return;
|
|
6084
|
+
}
|
|
6085
|
+
if (!session.subscribers.has(subscriberKey)) return;
|
|
6086
|
+
session.subscribers.delete(subscriberKey);
|
|
4212
6087
|
const remaining = session.watcher.removeSubscriber();
|
|
4213
6088
|
if (remaining > 0) {
|
|
4214
6089
|
log7.info("viewer left transcript watcher; other viewers still subscribed", {
|
|
4215
6090
|
sessionId,
|
|
6091
|
+
viewerId,
|
|
4216
6092
|
remaining
|
|
4217
6093
|
});
|
|
4218
6094
|
return;
|
|
@@ -4224,74 +6100,164 @@ var TranscriptWatcherManager = class {
|
|
|
4224
6100
|
}
|
|
4225
6101
|
/** Periodically check if the transcript file has changed (new conversation). */
|
|
4226
6102
|
startRediscovery(sessionId, agentType) {
|
|
4227
|
-
this.
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
6103
|
+
this.rediscoverySessions.set(sessionId, agentType);
|
|
6104
|
+
this.rediscoveryCursor = 0;
|
|
6105
|
+
this.rediscoveryIndex = void 0;
|
|
6106
|
+
this.scheduleRediscovery(true);
|
|
6107
|
+
}
|
|
6108
|
+
scheduleRediscovery(restart = false) {
|
|
6109
|
+
if (restart && this.rediscoveryTimer) {
|
|
6110
|
+
clearTimeout(this.rediscoveryTimer);
|
|
6111
|
+
this.rediscoveryTimer = void 0;
|
|
6112
|
+
}
|
|
6113
|
+
if (this.rediscoveryTimer || this.rediscoverySessions.size === 0) return;
|
|
6114
|
+
const delay2 = Math.max(
|
|
6115
|
+
MIN_REDISCOVERY_STAGGER_MS,
|
|
6116
|
+
Math.ceil(REDISCOVERY_INTERVAL_MS / this.rediscoverySessions.size)
|
|
6117
|
+
);
|
|
6118
|
+
this.rediscoveryTimer = setTimeout(() => {
|
|
6119
|
+
this.rediscoveryTimer = void 0;
|
|
6120
|
+
this.runRediscoveryTick();
|
|
6121
|
+
}, delay2);
|
|
6122
|
+
if (typeof this.rediscoveryTimer.unref === "function") {
|
|
6123
|
+
this.rediscoveryTimer.unref();
|
|
6124
|
+
}
|
|
6125
|
+
}
|
|
6126
|
+
runRediscoveryTick() {
|
|
6127
|
+
for (const sessionId2 of this.rediscoverySessions.keys()) {
|
|
6128
|
+
if (!this.active.has(sessionId2)) {
|
|
6129
|
+
this.rediscoverySessions.delete(sessionId2);
|
|
4233
6130
|
}
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
6131
|
+
}
|
|
6132
|
+
const sessionIds = [...this.rediscoverySessions.keys()];
|
|
6133
|
+
if (sessionIds.length === 0) {
|
|
6134
|
+
this.rediscoveryCursor = 0;
|
|
6135
|
+
this.rediscoveryIndex = void 0;
|
|
6136
|
+
return;
|
|
6137
|
+
}
|
|
6138
|
+
if (this.rediscoveryCursor >= sessionIds.length) {
|
|
6139
|
+
this.rediscoveryCursor = 0;
|
|
6140
|
+
this.rediscoveryIndex = void 0;
|
|
6141
|
+
}
|
|
6142
|
+
const sessionId = sessionIds[this.rediscoveryCursor];
|
|
6143
|
+
const agentType = this.rediscoverySessions.get(sessionId);
|
|
6144
|
+
const discoveryIndex = this.rediscoveryIndex ?? createTranscriptDiscoveryIndex();
|
|
6145
|
+
this.rediscoveryIndex = discoveryIndex;
|
|
6146
|
+
this.rediscoverSession(sessionId, agentType, discoveryIndex);
|
|
6147
|
+
this.rediscoveryCursor++;
|
|
6148
|
+
if (this.rediscoveryCursor >= sessionIds.length) {
|
|
6149
|
+
this.rediscoveryCursor = 0;
|
|
6150
|
+
this.rediscoveryIndex = void 0;
|
|
6151
|
+
}
|
|
6152
|
+
this.scheduleRediscovery();
|
|
6153
|
+
}
|
|
6154
|
+
rediscoverSession(sessionId, agentType, discoveryIndex) {
|
|
6155
|
+
const session = this.active.get(sessionId);
|
|
6156
|
+
if (!session) return;
|
|
6157
|
+
const { path: newPath } = discoverTranscriptFileDetailed(
|
|
6158
|
+
sessionId,
|
|
6159
|
+
agentType,
|
|
6160
|
+
this.ownedPathsExcept(sessionId),
|
|
6161
|
+
discoveryIndex
|
|
6162
|
+
);
|
|
6163
|
+
if (!newPath || newPath === session.filePath) return;
|
|
6164
|
+
log7.info("transcript file changed", { sessionId, newPath });
|
|
6165
|
+
const parser = getParser(agentType);
|
|
6166
|
+
if (!parser) return;
|
|
6167
|
+
const newV2Parser = getParserV2(agentType);
|
|
6168
|
+
const newWatcher = new TranscriptWatcher(
|
|
6169
|
+
newPath,
|
|
6170
|
+
parser,
|
|
6171
|
+
{
|
|
6172
|
+
onTurn: (turn, seq) => {
|
|
6173
|
+
if (this.hasLegacySubscribers(sessionId)) {
|
|
4250
6174
|
this.sendFn(sessionId, turn, seq);
|
|
4251
|
-
}
|
|
4252
|
-
|
|
6175
|
+
}
|
|
6176
|
+
},
|
|
6177
|
+
onOps: this.sendOpsFn ? (ops, seq) => {
|
|
6178
|
+
if (this.hasV2Subscribers(sessionId)) {
|
|
4253
6179
|
this.sendOpsFn(sessionId, ops, seq);
|
|
4254
|
-
}
|
|
4255
|
-
|
|
6180
|
+
}
|
|
6181
|
+
} : void 0,
|
|
6182
|
+
onTurnsSnapshot: (turns, seq) => {
|
|
6183
|
+
if (this.hasLegacySubscribers(sessionId)) {
|
|
4256
6184
|
this.sendSnapshotFn(sessionId, turns, seq);
|
|
4257
|
-
},
|
|
4258
|
-
onSnapshot: this.sendV2SnapshotFn ? (snapshot, seq) => {
|
|
4259
|
-
this.sendV2SnapshotFn(sessionId, snapshot, seq);
|
|
4260
|
-
} : void 0,
|
|
4261
|
-
onError: (error) => {
|
|
4262
|
-
log7.warn("transcript watcher error", { sessionId, error: String(error) });
|
|
4263
6185
|
}
|
|
4264
6186
|
},
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
6187
|
+
onSnapshot: this.sendV2SnapshotFn ? (snapshot, seq, reset) => {
|
|
6188
|
+
if (this.hasV2Subscribers(sessionId)) {
|
|
6189
|
+
this.sendV2SnapshotFn(sessionId, snapshot, seq, reset);
|
|
6190
|
+
}
|
|
6191
|
+
} : void 0,
|
|
6192
|
+
shouldProcessV1: () => this.hasLegacySubscribers(sessionId),
|
|
6193
|
+
shouldProcessV2: () => this.hasV2Subscribers(sessionId),
|
|
6194
|
+
onError: (error) => {
|
|
6195
|
+
log7.warn("transcript watcher error", {
|
|
6196
|
+
sessionId,
|
|
6197
|
+
error: String(error)
|
|
6198
|
+
});
|
|
6199
|
+
}
|
|
6200
|
+
},
|
|
6201
|
+
newV2Parser,
|
|
6202
|
+
sessionId
|
|
6203
|
+
);
|
|
6204
|
+
if (!newWatcher.start()) {
|
|
6205
|
+
log7.warn("failed to start watching new transcript", { newPath });
|
|
6206
|
+
return;
|
|
6207
|
+
}
|
|
6208
|
+
for (let i = 1; i < session.subscribers.size; i++) {
|
|
6209
|
+
newWatcher.addSubscriber();
|
|
6210
|
+
}
|
|
6211
|
+
session.watcher.stop();
|
|
6212
|
+
this.active.set(sessionId, {
|
|
6213
|
+
watcher: newWatcher,
|
|
6214
|
+
filePath: newPath,
|
|
6215
|
+
agentType: agentType ?? "",
|
|
6216
|
+
subscribers: session.subscribers
|
|
6217
|
+
});
|
|
6218
|
+
this.clearTranscriptState(sessionId);
|
|
6219
|
+
newWatcher.replayFromStart();
|
|
4280
6220
|
}
|
|
4281
|
-
/**
|
|
6221
|
+
/** Remove a session from the shared re-discovery scheduler. */
|
|
4282
6222
|
stopRediscovery(sessionId) {
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
}
|
|
6223
|
+
if (!this.rediscoverySessions.delete(sessionId)) return;
|
|
6224
|
+
this.rediscoveryCursor = 0;
|
|
6225
|
+
this.rediscoveryIndex = void 0;
|
|
6226
|
+
this.scheduleRediscovery(true);
|
|
4288
6227
|
}
|
|
4289
|
-
/**
|
|
4290
|
-
|
|
6228
|
+
/**
|
|
6229
|
+
* Handle sync-markdown. V2 sync is always snapshot-only because the relay
|
|
6230
|
+
* group is broadcast and the portal has no logical-op deduplication. A
|
|
6231
|
+
* future `fromSeq` is therefore also answered with current idempotent state.
|
|
6232
|
+
* Callers without a schema keep the legacy dual-snapshot behavior.
|
|
6233
|
+
*/
|
|
6234
|
+
handleSyncRequest(sessionId, fromSeq, schemaVersion, viewerId) {
|
|
4291
6235
|
const session = this.active.get(sessionId);
|
|
4292
6236
|
if (!session) return;
|
|
4293
|
-
|
|
6237
|
+
if (viewerId) {
|
|
6238
|
+
const subscriberKey = viewerId;
|
|
6239
|
+
if (session.subscribers.has(subscriberKey)) {
|
|
6240
|
+
const requestedSchema = schemaVersion === 2 ? 2 : schemaVersion === 1 ? 1 : 0;
|
|
6241
|
+
session.subscribers.set(subscriberKey, requestedSchema);
|
|
6242
|
+
const intent = this.desiredSubscriptions.get(sessionId)?.get(subscriberKey);
|
|
6243
|
+
if (intent) intent.schemaVersion = requestedSchema;
|
|
6244
|
+
}
|
|
6245
|
+
}
|
|
6246
|
+
void fromSeq;
|
|
6247
|
+
if (schemaVersion === 2) {
|
|
6248
|
+
if (this.sendV2SnapshotFn) {
|
|
6249
|
+
const snapshot = session.watcher.getSnapshot();
|
|
6250
|
+
this.sendV2SnapshotFn(
|
|
6251
|
+
sessionId,
|
|
6252
|
+
snapshot,
|
|
6253
|
+
session.watcher.currentSeq
|
|
6254
|
+
);
|
|
6255
|
+
}
|
|
6256
|
+
return;
|
|
6257
|
+
}
|
|
6258
|
+
const turns = session.watcher.getReplayTurns();
|
|
4294
6259
|
this.sendSnapshotFn(sessionId, turns, session.watcher.currentSeq);
|
|
6260
|
+
if (schemaVersion === 1) return;
|
|
4295
6261
|
if (this.sendV2SnapshotFn) {
|
|
4296
6262
|
const snapshot = session.watcher.getSnapshot();
|
|
4297
6263
|
this.sendV2SnapshotFn(sessionId, snapshot, session.watcher.currentSeq);
|
|
@@ -4303,15 +6269,16 @@ var TranscriptWatcherManager = class {
|
|
|
4303
6269
|
* and is the prime drop victim, but — unlike tmux-pipe terminal streams,
|
|
4304
6270
|
* which resync via capture-pane — the markdown channel had no recovery path,
|
|
4305
6271
|
* so a dropped snapshot left the read view permanently partial until a
|
|
4306
|
-
* reconnect. Re-emit
|
|
4307
|
-
*
|
|
4308
|
-
* re-saturate the queue.
|
|
6272
|
+
* reconnect. Re-emit bounded V1 and/or V2 snapshots according to active
|
|
6273
|
+
* subscriber capabilities, draining one session per tick so recovery does
|
|
6274
|
+
* not immediately re-saturate the queue.
|
|
4309
6275
|
*/
|
|
4310
6276
|
resyncMarkdownAfterDrop() {
|
|
4311
|
-
if (!this.sendV2SnapshotFn) return;
|
|
4312
6277
|
let queued = false;
|
|
4313
6278
|
for (const [sessionId, session] of this.active) {
|
|
4314
|
-
|
|
6279
|
+
const legacyPopulated = this.hasLegacySubscribers(sessionId) && session.watcher.getReplayTurns().length > 0;
|
|
6280
|
+
const v2Subscribed = this.hasV2Subscribers(sessionId) && Boolean(this.sendV2SnapshotFn);
|
|
6281
|
+
if (legacyPopulated || v2Subscribed) {
|
|
4315
6282
|
this.pendingMarkdownResync.add(sessionId);
|
|
4316
6283
|
queued = true;
|
|
4317
6284
|
}
|
|
@@ -4331,12 +6298,22 @@ var TranscriptWatcherManager = class {
|
|
|
4331
6298
|
const sessionId = next.value;
|
|
4332
6299
|
this.pendingMarkdownResync.delete(sessionId);
|
|
4333
6300
|
const session = this.active.get(sessionId);
|
|
4334
|
-
if (session
|
|
4335
|
-
this.
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
6301
|
+
if (session) {
|
|
6302
|
+
if (this.hasLegacySubscribers(sessionId)) {
|
|
6303
|
+
this.sendSnapshotFn(
|
|
6304
|
+
sessionId,
|
|
6305
|
+
session.watcher.getReplayTurns(),
|
|
6306
|
+
session.watcher.currentSeq
|
|
6307
|
+
);
|
|
6308
|
+
}
|
|
6309
|
+
if (this.hasV2Subscribers(sessionId) && this.sendV2SnapshotFn) {
|
|
6310
|
+
this.sendV2SnapshotFn(
|
|
6311
|
+
sessionId,
|
|
6312
|
+
session.watcher.getSnapshot(),
|
|
6313
|
+
session.watcher.currentSeq,
|
|
6314
|
+
true
|
|
6315
|
+
);
|
|
6316
|
+
}
|
|
4340
6317
|
}
|
|
4341
6318
|
}, MARKDOWN_RESYNC_DRAIN_MS);
|
|
4342
6319
|
if (typeof this.markdownResyncTimer.unref === "function") {
|
|
@@ -4355,16 +6332,22 @@ var TranscriptWatcherManager = class {
|
|
|
4355
6332
|
this.stopMarkdownResync();
|
|
4356
6333
|
for (const [sessionId, session] of this.active) {
|
|
4357
6334
|
session.watcher.stop();
|
|
4358
|
-
this.stopRediscovery(sessionId);
|
|
4359
6335
|
log7.info("stopped transcript watcher", { sessionId });
|
|
4360
6336
|
}
|
|
4361
6337
|
this.active.clear();
|
|
4362
|
-
this.
|
|
6338
|
+
this.rediscoverySessions.clear();
|
|
6339
|
+
if (this.rediscoveryTimer) {
|
|
6340
|
+
clearTimeout(this.rediscoveryTimer);
|
|
6341
|
+
this.rediscoveryTimer = void 0;
|
|
6342
|
+
}
|
|
6343
|
+
this.rediscoveryCursor = 0;
|
|
6344
|
+
this.rediscoveryIndex = void 0;
|
|
4363
6345
|
for (const timer of this.enableRetryTimers.values()) {
|
|
4364
6346
|
clearTimeout(timer);
|
|
4365
6347
|
}
|
|
4366
6348
|
this.enableRetryTimers.clear();
|
|
4367
6349
|
this.enableRetryAttempts.clear();
|
|
6350
|
+
this.desiredSubscriptions.clear();
|
|
4368
6351
|
}
|
|
4369
6352
|
/** Check if a session has an active watcher. */
|
|
4370
6353
|
isWatching(sessionId) {
|
|
@@ -9378,6 +11361,80 @@ var KIND_TO_BINARY_LABEL = {
|
|
|
9378
11361
|
codex: "Codex"
|
|
9379
11362
|
};
|
|
9380
11363
|
var activeSessions = /* @__PURE__ */ new Map();
|
|
11364
|
+
var MAX_NATIVE_RECOVERY_TOMBSTONES = 32;
|
|
11365
|
+
var nativeRecoverySnapshots = /* @__PURE__ */ new Map();
|
|
11366
|
+
function retainNativeRecoverySnapshot(session) {
|
|
11367
|
+
const snapshot = trimSnapshot(session.snapshot);
|
|
11368
|
+
nativeRecoverySnapshots.delete(session.sessionId);
|
|
11369
|
+
nativeRecoverySnapshots.set(session.sessionId, {
|
|
11370
|
+
snapshot,
|
|
11371
|
+
seq: session.seq
|
|
11372
|
+
});
|
|
11373
|
+
while (nativeRecoverySnapshots.size > MAX_NATIVE_RECOVERY_TOMBSTONES) {
|
|
11374
|
+
const oldest = nativeRecoverySnapshots.keys().next().value;
|
|
11375
|
+
if (oldest === void 0) break;
|
|
11376
|
+
nativeRecoverySnapshots.delete(oldest);
|
|
11377
|
+
}
|
|
11378
|
+
}
|
|
11379
|
+
function publishNativeOps(session, ops, sendOps) {
|
|
11380
|
+
try {
|
|
11381
|
+
let nextSnapshot = session.snapshot;
|
|
11382
|
+
for (const batch of boundTranscriptOpsForWire(ops)) {
|
|
11383
|
+
nextSnapshot = applyOps(nextSnapshot, batch);
|
|
11384
|
+
}
|
|
11385
|
+
session.snapshot = nextSnapshot;
|
|
11386
|
+
} catch (error) {
|
|
11387
|
+
log14.warn("native transcript state update failed; delta omitted", {
|
|
11388
|
+
sessionId: session.sessionId,
|
|
11389
|
+
error: error instanceof Error ? error.message : String(error)
|
|
11390
|
+
});
|
|
11391
|
+
return;
|
|
11392
|
+
}
|
|
11393
|
+
session.seq += 1;
|
|
11394
|
+
try {
|
|
11395
|
+
sendOps(session.sessionId, ops, session.seq);
|
|
11396
|
+
} catch (error) {
|
|
11397
|
+
log14.warn("native transcript delta send failed; snapshot retained for recovery", {
|
|
11398
|
+
sessionId: session.sessionId,
|
|
11399
|
+
seq: session.seq,
|
|
11400
|
+
error: error instanceof Error ? error.message : String(error)
|
|
11401
|
+
});
|
|
11402
|
+
}
|
|
11403
|
+
}
|
|
11404
|
+
function getNativeTranscriptSnapshot(sessionId) {
|
|
11405
|
+
const session = activeSessions.get(sessionId);
|
|
11406
|
+
if (session) {
|
|
11407
|
+
return {
|
|
11408
|
+
snapshot: trimSnapshot(session.snapshot),
|
|
11409
|
+
seq: session.seq
|
|
11410
|
+
};
|
|
11411
|
+
}
|
|
11412
|
+
const recovery = nativeRecoverySnapshots.get(sessionId);
|
|
11413
|
+
if (!recovery) return null;
|
|
11414
|
+
return {
|
|
11415
|
+
snapshot: trimSnapshot(recovery.snapshot),
|
|
11416
|
+
seq: recovery.seq
|
|
11417
|
+
};
|
|
11418
|
+
}
|
|
11419
|
+
function syncNativeTranscriptSnapshot(sessionId, sendSnapshot, forceReset = false) {
|
|
11420
|
+
const current = getNativeTranscriptSnapshot(sessionId);
|
|
11421
|
+
if (!current) return false;
|
|
11422
|
+
const reset = forceReset || current.snapshot.turnOrder.length === 0;
|
|
11423
|
+
sendSnapshot(
|
|
11424
|
+
sessionId,
|
|
11425
|
+
current.snapshot,
|
|
11426
|
+
current.seq,
|
|
11427
|
+
reset ? true : void 0
|
|
11428
|
+
);
|
|
11429
|
+
return true;
|
|
11430
|
+
}
|
|
11431
|
+
function getNativeTranscriptSnapshotSessionIds() {
|
|
11432
|
+
const ids = [...activeSessions.keys()];
|
|
11433
|
+
for (const sessionId of nativeRecoverySnapshots.keys()) {
|
|
11434
|
+
if (!activeSessions.has(sessionId)) ids.push(sessionId);
|
|
11435
|
+
}
|
|
11436
|
+
return ids;
|
|
11437
|
+
}
|
|
9381
11438
|
var defaultDeps = {
|
|
9382
11439
|
detect: detectNativeCapabilities,
|
|
9383
11440
|
startClaude: startClaudeNativeStream,
|
|
@@ -9502,8 +11559,10 @@ async function launchClaudeNative(options, sendOps) {
|
|
|
9502
11559
|
command: `claude -p (native)`,
|
|
9503
11560
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9504
11561
|
seq: 0,
|
|
11562
|
+
snapshot: createEmptySnapshot(sessionId),
|
|
9505
11563
|
handle: null
|
|
9506
11564
|
};
|
|
11565
|
+
nativeRecoverySnapshots.delete(sessionId);
|
|
9507
11566
|
activeSessions.set(sessionId, session);
|
|
9508
11567
|
const extraEnv = deps.buildExtraEnv();
|
|
9509
11568
|
try {
|
|
@@ -9519,8 +11578,9 @@ async function launchClaudeNative(options, sendOps) {
|
|
|
9519
11578
|
},
|
|
9520
11579
|
{
|
|
9521
11580
|
onOps: (ops) => {
|
|
9522
|
-
|
|
9523
|
-
|
|
11581
|
+
if (activeSessions.get(sessionId) === session) {
|
|
11582
|
+
publishNativeOps(session, ops, sendOps);
|
|
11583
|
+
}
|
|
9524
11584
|
},
|
|
9525
11585
|
onWarn: (msg) => {
|
|
9526
11586
|
log14.warn("claude native warn", { sessionId, msg });
|
|
@@ -9539,16 +11599,16 @@ async function launchClaudeNative(options, sendOps) {
|
|
|
9539
11599
|
log14.warn("claude native breaker opened; removing inventory entry", {
|
|
9540
11600
|
sessionId
|
|
9541
11601
|
});
|
|
9542
|
-
if (activeSessions.
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
sessionId,
|
|
11602
|
+
if (activeSessions.get(sessionId) === session) {
|
|
11603
|
+
publishNativeOps(
|
|
11604
|
+
session,
|
|
9546
11605
|
buildFallbackStatusOps(
|
|
9547
11606
|
"Claude native stream stopped after repeated failures. Restart the session to retry.",
|
|
9548
11607
|
"error"
|
|
9549
11608
|
),
|
|
9550
|
-
|
|
11609
|
+
sendOps
|
|
9551
11610
|
);
|
|
11611
|
+
retainNativeRecoverySnapshot(session);
|
|
9552
11612
|
activeSessions.delete(sessionId);
|
|
9553
11613
|
}
|
|
9554
11614
|
}
|
|
@@ -9556,6 +11616,7 @@ async function launchClaudeNative(options, sendOps) {
|
|
|
9556
11616
|
);
|
|
9557
11617
|
} catch (err) {
|
|
9558
11618
|
activeSessions.delete(sessionId);
|
|
11619
|
+
nativeRecoverySnapshots.delete(sessionId);
|
|
9559
11620
|
const detail = err instanceof Error ? err.message : String(err);
|
|
9560
11621
|
log14.error("claude native launch threw", { sessionId, detail });
|
|
9561
11622
|
return {
|
|
@@ -9640,8 +11701,10 @@ async function launchGeminiNative(options, sendOps) {
|
|
|
9640
11701
|
command: `gemini -p (native)`,
|
|
9641
11702
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9642
11703
|
seq: 0,
|
|
11704
|
+
snapshot: createEmptySnapshot(sessionId),
|
|
9643
11705
|
handle: null
|
|
9644
11706
|
};
|
|
11707
|
+
nativeRecoverySnapshots.delete(sessionId);
|
|
9645
11708
|
activeSessions.set(sessionId, session);
|
|
9646
11709
|
const extraEnv = deps.buildGeminiExtraEnv();
|
|
9647
11710
|
try {
|
|
@@ -9656,8 +11719,9 @@ async function launchGeminiNative(options, sendOps) {
|
|
|
9656
11719
|
},
|
|
9657
11720
|
{
|
|
9658
11721
|
onOps: (ops) => {
|
|
9659
|
-
|
|
9660
|
-
|
|
11722
|
+
if (activeSessions.get(sessionId) === session) {
|
|
11723
|
+
publishNativeOps(session, ops, sendOps);
|
|
11724
|
+
}
|
|
9661
11725
|
},
|
|
9662
11726
|
onWarn: (msg) => {
|
|
9663
11727
|
log14.warn("gemini native warn", { sessionId, msg });
|
|
@@ -9672,16 +11736,16 @@ async function launchGeminiNative(options, sendOps) {
|
|
|
9672
11736
|
log14.warn("gemini native breaker opened; removing inventory entry", {
|
|
9673
11737
|
sessionId
|
|
9674
11738
|
});
|
|
9675
|
-
if (activeSessions.
|
|
9676
|
-
|
|
9677
|
-
|
|
9678
|
-
sessionId,
|
|
11739
|
+
if (activeSessions.get(sessionId) === session) {
|
|
11740
|
+
publishNativeOps(
|
|
11741
|
+
session,
|
|
9679
11742
|
buildFallbackStatusOps(
|
|
9680
11743
|
"Gemini native stream stopped after repeated failures. Restart the session to retry.",
|
|
9681
11744
|
"error"
|
|
9682
11745
|
),
|
|
9683
|
-
|
|
11746
|
+
sendOps
|
|
9684
11747
|
);
|
|
11748
|
+
retainNativeRecoverySnapshot(session);
|
|
9685
11749
|
activeSessions.delete(sessionId);
|
|
9686
11750
|
}
|
|
9687
11751
|
}
|
|
@@ -9689,6 +11753,7 @@ async function launchGeminiNative(options, sendOps) {
|
|
|
9689
11753
|
);
|
|
9690
11754
|
} catch (err) {
|
|
9691
11755
|
activeSessions.delete(sessionId);
|
|
11756
|
+
nativeRecoverySnapshots.delete(sessionId);
|
|
9692
11757
|
const detail = err instanceof Error ? err.message : String(err);
|
|
9693
11758
|
log14.error("gemini native launch threw", { sessionId, detail });
|
|
9694
11759
|
return {
|
|
@@ -9774,8 +11839,10 @@ async function launchCodexNative(options, sendOps) {
|
|
|
9774
11839
|
command: transport === "app-server" ? `codex app-server (native)` : `codex exec (native)`,
|
|
9775
11840
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9776
11841
|
seq: 0,
|
|
11842
|
+
snapshot: createEmptySnapshot(sessionId),
|
|
9777
11843
|
handle: null
|
|
9778
11844
|
};
|
|
11845
|
+
nativeRecoverySnapshots.delete(sessionId);
|
|
9779
11846
|
activeSessions.set(sessionId, session);
|
|
9780
11847
|
const extraEnv = transport === "app-server" ? buildCodexAppServerExtraEnv() : deps.buildCodexExtraEnv();
|
|
9781
11848
|
const onBreakerOpenHandler = () => {
|
|
@@ -9783,16 +11850,16 @@ async function launchCodexNative(options, sendOps) {
|
|
|
9783
11850
|
sessionId,
|
|
9784
11851
|
transport
|
|
9785
11852
|
});
|
|
9786
|
-
if (activeSessions.
|
|
9787
|
-
|
|
9788
|
-
|
|
9789
|
-
sessionId,
|
|
11853
|
+
if (activeSessions.get(sessionId) === session) {
|
|
11854
|
+
publishNativeOps(
|
|
11855
|
+
session,
|
|
9790
11856
|
buildFallbackStatusOps(
|
|
9791
11857
|
"Codex native stream stopped after repeated failures. Restart the session to retry.",
|
|
9792
11858
|
"error"
|
|
9793
11859
|
),
|
|
9794
|
-
|
|
11860
|
+
sendOps
|
|
9795
11861
|
);
|
|
11862
|
+
retainNativeRecoverySnapshot(session);
|
|
9796
11863
|
activeSessions.delete(sessionId);
|
|
9797
11864
|
}
|
|
9798
11865
|
};
|
|
@@ -9812,8 +11879,9 @@ async function launchCodexNative(options, sendOps) {
|
|
|
9812
11879
|
},
|
|
9813
11880
|
{
|
|
9814
11881
|
onOps: (ops) => {
|
|
9815
|
-
|
|
9816
|
-
|
|
11882
|
+
if (activeSessions.get(sessionId) === session) {
|
|
11883
|
+
publishNativeOps(session, ops, sendOps);
|
|
11884
|
+
}
|
|
9817
11885
|
},
|
|
9818
11886
|
onWarn: (msg) => {
|
|
9819
11887
|
log14.warn("codex app-server warn", { sessionId, msg });
|
|
@@ -9840,8 +11908,9 @@ async function launchCodexNative(options, sendOps) {
|
|
|
9840
11908
|
},
|
|
9841
11909
|
{
|
|
9842
11910
|
onOps: (ops) => {
|
|
9843
|
-
|
|
9844
|
-
|
|
11911
|
+
if (activeSessions.get(sessionId) === session) {
|
|
11912
|
+
publishNativeOps(session, ops, sendOps);
|
|
11913
|
+
}
|
|
9845
11914
|
},
|
|
9846
11915
|
onWarn: (msg) => {
|
|
9847
11916
|
log14.warn("codex native warn", { sessionId, msg });
|
|
@@ -9858,6 +11927,7 @@ async function launchCodexNative(options, sendOps) {
|
|
|
9858
11927
|
}
|
|
9859
11928
|
} catch (err) {
|
|
9860
11929
|
activeSessions.delete(sessionId);
|
|
11930
|
+
nativeRecoverySnapshots.delete(sessionId);
|
|
9861
11931
|
const detail = err instanceof Error ? err.message : String(err);
|
|
9862
11932
|
log14.error("codex native launch threw", { sessionId, detail });
|
|
9863
11933
|
return {
|
|
@@ -9884,6 +11954,7 @@ async function launchCodexNative(options, sendOps) {
|
|
|
9884
11954
|
} catch {
|
|
9885
11955
|
}
|
|
9886
11956
|
activeSessions.delete(sessionId);
|
|
11957
|
+
nativeRecoverySnapshots.delete(sessionId);
|
|
9887
11958
|
return {
|
|
9888
11959
|
ok: false,
|
|
9889
11960
|
kind,
|
|
@@ -9910,7 +11981,8 @@ async function launchCodexNative(options, sendOps) {
|
|
|
9910
11981
|
}
|
|
9911
11982
|
function stopNativeSession(sessionId) {
|
|
9912
11983
|
const session = activeSessions.get(sessionId);
|
|
9913
|
-
|
|
11984
|
+
const removedRecovery = nativeRecoverySnapshots.delete(sessionId);
|
|
11985
|
+
if (!session) return removedRecovery;
|
|
9914
11986
|
try {
|
|
9915
11987
|
session.handle?.stop();
|
|
9916
11988
|
} catch (err) {
|
|
@@ -9926,6 +11998,7 @@ function stopNativeSession(sessionId) {
|
|
|
9926
11998
|
function stopAllNativeSessions() {
|
|
9927
11999
|
const ids = [...activeSessions.keys()];
|
|
9928
12000
|
for (const id of ids) stopNativeSession(id);
|
|
12001
|
+
nativeRecoverySnapshots.clear();
|
|
9929
12002
|
}
|
|
9930
12003
|
function isNativeSession(sessionId) {
|
|
9931
12004
|
for (const prefix of Object.values(KIND_TO_SESSION_PREFIX)) {
|
|
@@ -10746,6 +12819,7 @@ async function claimHost(params) {
|
|
|
10746
12819
|
return data;
|
|
10747
12820
|
}
|
|
10748
12821
|
async function negotiateAgent(params) {
|
|
12822
|
+
const requestStartedAt = Date.now();
|
|
10749
12823
|
const response = await fetch(`${params.portal}/api/agent/negotiate`, {
|
|
10750
12824
|
method: "POST",
|
|
10751
12825
|
headers: {
|
|
@@ -10754,6 +12828,7 @@ async function negotiateAgent(params) {
|
|
|
10754
12828
|
},
|
|
10755
12829
|
body: JSON.stringify({})
|
|
10756
12830
|
});
|
|
12831
|
+
const responseReceivedAt = Date.now();
|
|
10757
12832
|
const data = await response.json();
|
|
10758
12833
|
if (response.status === 401 || response.status === 403) {
|
|
10759
12834
|
throw new AuthError(
|
|
@@ -10766,6 +12841,11 @@ async function negotiateAgent(params) {
|
|
|
10766
12841
|
if (!data.url || !data.groups?.privateGroup || !data.groups?.registryGroup) {
|
|
10767
12842
|
throw new Error("Negotiate response missing websocket URL or group info");
|
|
10768
12843
|
}
|
|
12844
|
+
delete data.serverClockOffsetMs;
|
|
12845
|
+
if (Number.isSafeInteger(data.serverTimeMs)) {
|
|
12846
|
+
const requestMidpoint = requestStartedAt + Math.round((responseReceivedAt - requestStartedAt) / 2);
|
|
12847
|
+
data.serverClockOffsetMs = data.serverTimeMs - requestMidpoint;
|
|
12848
|
+
}
|
|
10769
12849
|
return data;
|
|
10770
12850
|
}
|
|
10771
12851
|
async function deregisterHost(params) {
|
|
@@ -11075,7 +13155,9 @@ var SessionStreamer = class {
|
|
|
11075
13155
|
* never fires, and `resyncStream` deliberately does NOT restart pipe-pane —
|
|
11076
13156
|
* so without this the pane is frozen until the stream is torn down by hand.
|
|
11077
13157
|
*
|
|
11078
|
-
*
|
|
13158
|
+
* Costs one batched `tmux list-panes -a` fork for pids, plus one `capture-pane`
|
|
13159
|
+
* fork ONLY per currently-silent stream (flowing streams are provably alive and
|
|
13160
|
+
* skip the hash) — so ~one fork/tick on a healthy host.
|
|
11079
13161
|
*/
|
|
11080
13162
|
checkPipeLiveness() {
|
|
11081
13163
|
const tmuxStreams = [...this.active.values()].filter(
|
|
@@ -11101,22 +13183,29 @@ var SessionStreamer = class {
|
|
|
11101
13183
|
continue;
|
|
11102
13184
|
}
|
|
11103
13185
|
const bytesNow = stream.bytesReceived ?? 0;
|
|
11104
|
-
const paneMoved = info.fingerprint !== stream.lastLivenessFingerprint;
|
|
11105
13186
|
const fifoSilent = bytesNow === (stream.lastLivenessBytes ?? 0);
|
|
11106
|
-
if (
|
|
11107
|
-
stream.deadPipeStrikes =
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11112
|
-
|
|
11113
|
-
|
|
13187
|
+
if (!fifoSilent) {
|
|
13188
|
+
stream.deadPipeStrikes = 0;
|
|
13189
|
+
} else {
|
|
13190
|
+
const contentHash = this.paneContentHash(stream.paneTarget, stream.cleanEnv);
|
|
13191
|
+
if (contentHash !== null) {
|
|
13192
|
+
const paneMoved = stream.lastContentHash !== void 0 && contentHash !== stream.lastContentHash;
|
|
13193
|
+
if (paneMoved) {
|
|
13194
|
+
stream.deadPipeStrikes = (stream.deadPipeStrikes ?? 0) + 1;
|
|
13195
|
+
if (stream.deadPipeStrikes >= DEAD_PIPE_STRIKES && !backedOff) {
|
|
13196
|
+
dead.push({
|
|
13197
|
+
sessionId: stream.sessionId,
|
|
13198
|
+
reason: "pipe-pane silent while pane active",
|
|
13199
|
+
detail: { strikes: stream.deadPipeStrikes }
|
|
13200
|
+
});
|
|
13201
|
+
}
|
|
13202
|
+
} else {
|
|
13203
|
+
stream.deadPipeStrikes = 0;
|
|
13204
|
+
}
|
|
13205
|
+
stream.lastContentHash = contentHash;
|
|
11114
13206
|
}
|
|
11115
|
-
} else {
|
|
11116
|
-
stream.deadPipeStrikes = 0;
|
|
11117
13207
|
}
|
|
11118
13208
|
stream.lastLivenessBytes = bytesNow;
|
|
11119
|
-
stream.lastLivenessFingerprint = info.fingerprint;
|
|
11120
13209
|
}
|
|
11121
13210
|
if (dead.length === 0) return;
|
|
11122
13211
|
const [first, ...rest] = dead;
|
|
@@ -11460,7 +13549,7 @@ var SessionStreamer = class {
|
|
|
11460
13549
|
try {
|
|
11461
13550
|
this.sendResetFn?.(sessionId, withScrollback ? "full" : "repaint", targetViewerId);
|
|
11462
13551
|
const alternateScreen = this.detectAlternateScreen(paneTarget, cleanEnv);
|
|
11463
|
-
if (withScrollback) {
|
|
13552
|
+
if (withScrollback && !alternateScreen) {
|
|
11464
13553
|
send("\x1B[3J");
|
|
11465
13554
|
try {
|
|
11466
13555
|
const scrollback = (0, import_node_child_process2.execFileSync)(
|
|
@@ -11505,6 +13594,31 @@ var SessionStreamer = class {
|
|
|
11505
13594
|
return false;
|
|
11506
13595
|
}
|
|
11507
13596
|
}
|
|
13597
|
+
/** Cheap stable string hash (djb2) for content change-detection. */
|
|
13598
|
+
hashString(s) {
|
|
13599
|
+
let h = 5381;
|
|
13600
|
+
for (let i = 0; i < s.length; i++) h = Math.imul(h, 33) + s.charCodeAt(i) | 0;
|
|
13601
|
+
return (h >>> 0).toString(36) + ":" + s.length;
|
|
13602
|
+
}
|
|
13603
|
+
/**
|
|
13604
|
+
* Hash of a pane's visible content (`capture-pane -p`). This is the watchdog's
|
|
13605
|
+
* activity signal: unlike cursor/history it changes on ANY visible redraw,
|
|
13606
|
+
* including in-place TUI updates (spinners, token counters, status bars) that
|
|
13607
|
+
* a Claude/Gemini pane emits without moving the cursor. Null on tmux failure
|
|
13608
|
+
* (the watchdog then skips the activity check for that tick).
|
|
13609
|
+
*/
|
|
13610
|
+
paneContentHash(paneTarget, cleanEnv) {
|
|
13611
|
+
try {
|
|
13612
|
+
const out = (0, import_node_child_process2.execFileSync)(
|
|
13613
|
+
"tmux",
|
|
13614
|
+
["capture-pane", "-t", paneTarget, "-p"],
|
|
13615
|
+
{ env: cleanEnv ?? this.tmuxEnv(), timeout: 5e3, encoding: "utf-8" }
|
|
13616
|
+
);
|
|
13617
|
+
return this.hashString(out);
|
|
13618
|
+
} catch {
|
|
13619
|
+
return null;
|
|
13620
|
+
}
|
|
13621
|
+
}
|
|
11508
13622
|
/** Build a tmux-safe env (TMUX/TMUX_PANE stripped) for liveness queries. */
|
|
11509
13623
|
tmuxEnv() {
|
|
11510
13624
|
const env = { ...process.env };
|
|
@@ -11562,9 +13676,9 @@ var SessionStreamer = class {
|
|
|
11562
13676
|
* would shift the viewport by 1 row. Returns the number of lines sent
|
|
11563
13677
|
* (0 when the capture failed or was empty) for relative cursor sync.
|
|
11564
13678
|
*/
|
|
11565
|
-
sendVisiblePane(send, paneTarget, cleanEnv,
|
|
13679
|
+
sendVisiblePane(send, paneTarget, cleanEnv, _alternateScreen) {
|
|
11566
13680
|
try {
|
|
11567
|
-
const captureArgs =
|
|
13681
|
+
const captureArgs = ["capture-pane", "-t", paneTarget, "-p", "-e"];
|
|
11568
13682
|
const initial = (0, import_node_child_process2.execFileSync)(
|
|
11569
13683
|
"tmux",
|
|
11570
13684
|
captureArgs,
|
|
@@ -11658,10 +13772,8 @@ var SessionStreamer = class {
|
|
|
11658
13772
|
return false;
|
|
11659
13773
|
}
|
|
11660
13774
|
const paneInfo = this.readPaneInfo(paneTarget, cleanEnv);
|
|
11661
|
-
if (paneInfo)
|
|
11662
|
-
|
|
11663
|
-
stream.lastLivenessFingerprint = paneInfo.fingerprint;
|
|
11664
|
-
}
|
|
13775
|
+
if (paneInfo) stream.panePid = paneInfo.panePid;
|
|
13776
|
+
stream.lastContentHash = this.paneContentHash(paneTarget, cleanEnv) ?? void 0;
|
|
11665
13777
|
stream.bytesReceived = 0;
|
|
11666
13778
|
stream.lastLivenessBytes = 0;
|
|
11667
13779
|
stream.deadPipeStrikes = 0;
|
|
@@ -11698,7 +13810,7 @@ var SessionStreamer = class {
|
|
|
11698
13810
|
this.sendResetFn?.(sessionId, "full");
|
|
11699
13811
|
const alternateScreen = this.detectAlternateScreen(paneTarget, cleanEnv);
|
|
11700
13812
|
stream.alternateScreen = alternateScreen;
|
|
11701
|
-
if (!opts?.skipScrollback) {
|
|
13813
|
+
if (!opts?.skipScrollback && !alternateScreen) {
|
|
11702
13814
|
this.sendFn(sessionId, "\x1B[3J");
|
|
11703
13815
|
try {
|
|
11704
13816
|
const scrollback = (0, import_node_child_process2.execFileSync)(
|
|
@@ -11979,7 +14091,104 @@ var SessionStreamer = class {
|
|
|
11979
14091
|
};
|
|
11980
14092
|
|
|
11981
14093
|
// src/crypto-channel.ts
|
|
11982
|
-
var
|
|
14094
|
+
var import_node_crypto2 = require("crypto");
|
|
14095
|
+
|
|
14096
|
+
// src/crypto-binding.ts
|
|
14097
|
+
var CRYPTO_BINDING_V2_VERSION = 2;
|
|
14098
|
+
var CRYPTO_BINDING_V2_DOMAIN = "ragent/aead-binding/v2";
|
|
14099
|
+
var CRYPTO_BINDING_V2_DIRECTION = "portal-to-agent";
|
|
14100
|
+
var CRYPTO_BINDING_V2_MAX_VIEWER_ID_BYTES = 128;
|
|
14101
|
+
var CRYPTO_BINDING_V2_MAX_SESSION_ID_BYTES = 4096;
|
|
14102
|
+
var UINT16_MAX = 65535;
|
|
14103
|
+
var UINT32_MAX = 4294967295;
|
|
14104
|
+
var encoder = new TextEncoder();
|
|
14105
|
+
function encodedField(value, name) {
|
|
14106
|
+
const bytes = encoder.encode(value);
|
|
14107
|
+
if (bytes.byteLength > UINT16_MAX) {
|
|
14108
|
+
throw new Error(`${name} exceeds the UInt16 length bound`);
|
|
14109
|
+
}
|
|
14110
|
+
return bytes;
|
|
14111
|
+
}
|
|
14112
|
+
function validateCryptoBindingV2(value) {
|
|
14113
|
+
if (!value || typeof value !== "object") {
|
|
14114
|
+
throw new Error("Crypto binding must be an object");
|
|
14115
|
+
}
|
|
14116
|
+
const binding = value;
|
|
14117
|
+
if (binding.bindingVersion !== CRYPTO_BINDING_V2_VERSION) {
|
|
14118
|
+
throw new Error("Unsupported crypto binding version");
|
|
14119
|
+
}
|
|
14120
|
+
if (binding.direction !== CRYPTO_BINDING_V2_DIRECTION) {
|
|
14121
|
+
throw new Error("Invalid crypto binding direction");
|
|
14122
|
+
}
|
|
14123
|
+
if (binding.type !== "input" && binding.type !== "control") {
|
|
14124
|
+
throw new Error("Invalid crypto binding message type");
|
|
14125
|
+
}
|
|
14126
|
+
if (binding.type === "input" && binding.action !== "" || binding.type === "control" && binding.action !== "upload-file") {
|
|
14127
|
+
throw new Error("Invalid crypto binding action for message type");
|
|
14128
|
+
}
|
|
14129
|
+
if (typeof binding.viewerId !== "string" || binding.viewerId.length === 0) {
|
|
14130
|
+
throw new Error("Crypto binding viewerId is required");
|
|
14131
|
+
}
|
|
14132
|
+
if (binding.viewerId.length > CRYPTO_BINDING_V2_MAX_VIEWER_ID_BYTES) {
|
|
14133
|
+
throw new Error(
|
|
14134
|
+
`Crypto binding viewerId exceeds ${CRYPTO_BINDING_V2_MAX_VIEWER_ID_BYTES} bytes`
|
|
14135
|
+
);
|
|
14136
|
+
}
|
|
14137
|
+
if (!/^[A-Za-z0-9:_-]+$/.test(binding.viewerId)) {
|
|
14138
|
+
throw new Error("Crypto binding viewerId contains unsupported characters");
|
|
14139
|
+
}
|
|
14140
|
+
if (typeof binding.sessionId !== "string" || binding.sessionId.length === 0) {
|
|
14141
|
+
throw new Error("Crypto binding sessionId is required");
|
|
14142
|
+
}
|
|
14143
|
+
if (binding.sessionId.length > CRYPTO_BINDING_V2_MAX_SESSION_ID_BYTES) {
|
|
14144
|
+
throw new Error(
|
|
14145
|
+
`Crypto binding sessionId exceeds ${CRYPTO_BINDING_V2_MAX_SESSION_ID_BYTES} bytes`
|
|
14146
|
+
);
|
|
14147
|
+
}
|
|
14148
|
+
if (encodedField(binding.viewerId, "viewerId").byteLength > CRYPTO_BINDING_V2_MAX_VIEWER_ID_BYTES) {
|
|
14149
|
+
throw new Error(
|
|
14150
|
+
`Crypto binding viewerId exceeds ${CRYPTO_BINDING_V2_MAX_VIEWER_ID_BYTES} bytes`
|
|
14151
|
+
);
|
|
14152
|
+
}
|
|
14153
|
+
if (encodedField(binding.sessionId, "sessionId").byteLength > CRYPTO_BINDING_V2_MAX_SESSION_ID_BYTES) {
|
|
14154
|
+
throw new Error(
|
|
14155
|
+
`Crypto binding sessionId exceeds ${CRYPTO_BINDING_V2_MAX_SESSION_ID_BYTES} bytes`
|
|
14156
|
+
);
|
|
14157
|
+
}
|
|
14158
|
+
if (!Number.isSafeInteger(binding.sentAtMs) || binding.sentAtMs < 0) {
|
|
14159
|
+
throw new Error("Crypto binding sentAtMs must be a non-negative safe integer");
|
|
14160
|
+
}
|
|
14161
|
+
if (!Number.isInteger(binding.seq) || binding.seq < 1 || binding.seq > UINT32_MAX) {
|
|
14162
|
+
throw new Error("Crypto binding seq must be a non-zero UInt32");
|
|
14163
|
+
}
|
|
14164
|
+
}
|
|
14165
|
+
function buildCryptoBindingV2(binding) {
|
|
14166
|
+
validateCryptoBindingV2(binding);
|
|
14167
|
+
const fields = [
|
|
14168
|
+
encodedField(CRYPTO_BINDING_V2_DOMAIN, "domain"),
|
|
14169
|
+
encodedField(binding.direction, "direction"),
|
|
14170
|
+
encodedField(binding.type, "type"),
|
|
14171
|
+
encodedField(binding.action, "action"),
|
|
14172
|
+
encodedField(binding.viewerId, "viewerId"),
|
|
14173
|
+
encodedField(binding.sessionId, "sessionId")
|
|
14174
|
+
];
|
|
14175
|
+
const byteLength = fields.reduce((total, field) => total + 2 + field.byteLength, 0) + 8 + 4;
|
|
14176
|
+
const result = new Uint8Array(byteLength);
|
|
14177
|
+
const view = new DataView(result.buffer);
|
|
14178
|
+
let offset = 0;
|
|
14179
|
+
for (const field of fields) {
|
|
14180
|
+
view.setUint16(offset, field.byteLength, false);
|
|
14181
|
+
offset += 2;
|
|
14182
|
+
result.set(field, offset);
|
|
14183
|
+
offset += field.byteLength;
|
|
14184
|
+
}
|
|
14185
|
+
view.setBigUint64(offset, BigInt(binding.sentAtMs), false);
|
|
14186
|
+
offset += 8;
|
|
14187
|
+
view.setUint32(offset, binding.seq, false);
|
|
14188
|
+
return result;
|
|
14189
|
+
}
|
|
14190
|
+
|
|
14191
|
+
// src/crypto-channel.ts
|
|
11983
14192
|
var log18 = createLogger("crypto-channel");
|
|
11984
14193
|
var AAD_BYTES = 4;
|
|
11985
14194
|
function deriveAesKey(sessionKey) {
|
|
@@ -11993,11 +14202,20 @@ function seqToAad(seq) {
|
|
|
11993
14202
|
buf.writeUInt32BE(seq, 0);
|
|
11994
14203
|
return buf;
|
|
11995
14204
|
}
|
|
11996
|
-
function
|
|
14205
|
+
function payloadAad(seq, binding) {
|
|
14206
|
+
if (!binding) return seqToAad(seq);
|
|
14207
|
+
if (binding.seq !== seq) {
|
|
14208
|
+
throw new Error(
|
|
14209
|
+
`Crypto binding seq ${binding.seq} does not match payload seq ${seq}`
|
|
14210
|
+
);
|
|
14211
|
+
}
|
|
14212
|
+
return buildCryptoBindingV2(binding);
|
|
14213
|
+
}
|
|
14214
|
+
function encryptPayload(data, sessionKey, seq, binding) {
|
|
11997
14215
|
const key = deriveAesKey(sessionKey);
|
|
11998
|
-
const iv = (0,
|
|
11999
|
-
const cipher = (0,
|
|
12000
|
-
cipher.setAAD(
|
|
14216
|
+
const iv = (0, import_node_crypto2.randomBytes)(12);
|
|
14217
|
+
const cipher = (0, import_node_crypto2.createCipheriv)("aes-256-gcm", key, iv);
|
|
14218
|
+
cipher.setAAD(payloadAad(seq, binding));
|
|
12001
14219
|
const encrypted = Buffer.concat([cipher.update(data, "utf-8"), cipher.final()]);
|
|
12002
14220
|
const authTag = cipher.getAuthTag();
|
|
12003
14221
|
const combined = Buffer.concat([encrypted, authTag]);
|
|
@@ -12007,7 +14225,7 @@ function encryptPayload(data, sessionKey, seq) {
|
|
|
12007
14225
|
seq
|
|
12008
14226
|
};
|
|
12009
14227
|
}
|
|
12010
|
-
function decryptPayload(enc, iv, sessionKey, seq) {
|
|
14228
|
+
function decryptPayload(enc, iv, sessionKey, seq, binding) {
|
|
12011
14229
|
try {
|
|
12012
14230
|
const key = deriveAesKey(sessionKey);
|
|
12013
14231
|
const ivBuf = Buffer.from(iv, "base64");
|
|
@@ -12015,8 +14233,8 @@ function decryptPayload(enc, iv, sessionKey, seq) {
|
|
|
12015
14233
|
if (combined.length < 16) return null;
|
|
12016
14234
|
const ciphertext = combined.subarray(0, combined.length - 16);
|
|
12017
14235
|
const authTag = combined.subarray(combined.length - 16);
|
|
12018
|
-
const decipher = (0,
|
|
12019
|
-
decipher.setAAD(
|
|
14236
|
+
const decipher = (0, import_node_crypto2.createDecipheriv)("aes-256-gcm", key, ivBuf);
|
|
14237
|
+
decipher.setAAD(payloadAad(seq, binding));
|
|
12020
14238
|
decipher.setAuthTag(authTag);
|
|
12021
14239
|
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
12022
14240
|
return decrypted.toString("utf-8");
|
|
@@ -12036,10 +14254,15 @@ var SequenceTracker = class {
|
|
|
12036
14254
|
this.last += 1;
|
|
12037
14255
|
return this.last;
|
|
12038
14256
|
}
|
|
12039
|
-
/** Receiver:
|
|
12040
|
-
|
|
14257
|
+
/** Receiver: validate without mutating replay state. */
|
|
14258
|
+
canAccept(seq) {
|
|
12041
14259
|
if (!Number.isInteger(seq) || seq < 1 || seq > 4294967295) return false;
|
|
12042
14260
|
if (seq <= this.last) return false;
|
|
14261
|
+
return true;
|
|
14262
|
+
}
|
|
14263
|
+
/** Receiver: commit an already-authenticated sequence number. */
|
|
14264
|
+
accept(seq) {
|
|
14265
|
+
if (!this.canAccept(seq)) return false;
|
|
12043
14266
|
this.last = seq;
|
|
12044
14267
|
return true;
|
|
12045
14268
|
}
|
|
@@ -12367,22 +14590,89 @@ var ShellManager = class {
|
|
|
12367
14590
|
var os7 = __toESM(require("os"));
|
|
12368
14591
|
var import_child_process5 = require("child_process");
|
|
12369
14592
|
var import_ws2 = __toESM(require("ws"));
|
|
14593
|
+
var INVENTORY_CACHE_TTL_MS = 1e3;
|
|
12370
14594
|
var InventoryManager = class {
|
|
12371
14595
|
lastSentFingerprint = "";
|
|
12372
14596
|
lastHttpHeartbeatAt = 0;
|
|
12373
14597
|
prevCpuSnapshot = null;
|
|
14598
|
+
cachedSessions = null;
|
|
14599
|
+
cachedSessionsAt = 0;
|
|
14600
|
+
cachedScope = "";
|
|
14601
|
+
inventoryRefresh = null;
|
|
14602
|
+
confirmedMisses = /* @__PURE__ */ new Map();
|
|
12374
14603
|
options;
|
|
12375
14604
|
constructor(options) {
|
|
12376
14605
|
this.options = options;
|
|
12377
14606
|
}
|
|
12378
14607
|
/** Update options (e.g., after token refresh). */
|
|
12379
14608
|
updateOptions(options) {
|
|
14609
|
+
const nextScope = `${options.hostId}\0${options.command}`;
|
|
14610
|
+
if (nextScope !== this.inventoryScope()) {
|
|
14611
|
+
this.cachedSessions = null;
|
|
14612
|
+
this.cachedSessionsAt = 0;
|
|
14613
|
+
this.cachedScope = "";
|
|
14614
|
+
this.confirmedMisses.clear();
|
|
14615
|
+
}
|
|
12380
14616
|
this.options = options;
|
|
12381
14617
|
}
|
|
14618
|
+
inventoryScope() {
|
|
14619
|
+
return `${this.options.hostId}\0${this.options.command}`;
|
|
14620
|
+
}
|
|
14621
|
+
cacheIsFresh(now2 = Date.now()) {
|
|
14622
|
+
return this.cachedSessions !== null && this.cachedScope === this.inventoryScope() && now2 - this.cachedSessionsAt < INVENTORY_CACHE_TTL_MS;
|
|
14623
|
+
}
|
|
14624
|
+
rememberInventory(sessions, scope = this.inventoryScope()) {
|
|
14625
|
+
this.cachedSessions = sessions;
|
|
14626
|
+
this.cachedSessionsAt = Date.now();
|
|
14627
|
+
this.cachedScope = scope;
|
|
14628
|
+
this.confirmedMisses.clear();
|
|
14629
|
+
return sessions;
|
|
14630
|
+
}
|
|
14631
|
+
/** Collect inventory at most once concurrently. Callers awaiting the same
|
|
14632
|
+
* refresh share both the process scan and its result. */
|
|
14633
|
+
refreshInventory() {
|
|
14634
|
+
const scope = this.inventoryScope();
|
|
14635
|
+
if (this.inventoryRefresh?.scope === scope) return this.inventoryRefresh.promise;
|
|
14636
|
+
const hostId = this.options.hostId;
|
|
14637
|
+
const command = this.options.command;
|
|
14638
|
+
const refresh = collectSessionInventory(hostId, command).then((sessions) => this.inventoryScope() === scope ? this.rememberInventory(sessions, scope) : sessions).finally(() => {
|
|
14639
|
+
if (this.inventoryRefresh?.promise === refresh) this.inventoryRefresh = null;
|
|
14640
|
+
});
|
|
14641
|
+
this.inventoryRefresh = { scope, promise: refresh };
|
|
14642
|
+
return refresh;
|
|
14643
|
+
}
|
|
14644
|
+
/** Return the recent inventory, refreshing it when stale or explicitly
|
|
14645
|
+
* requested. Primarily public so control paths can share the scan cache. */
|
|
14646
|
+
async getSessionInventory(forceRefresh = false) {
|
|
14647
|
+
if (!forceRefresh && this.cacheIsFresh()) return this.cachedSessions;
|
|
14648
|
+
return this.refreshInventory();
|
|
14649
|
+
}
|
|
14650
|
+
/** Find a session using the recent inventory. A fresh cache miss gets one
|
|
14651
|
+
* refresh so a just-created pane is not hidden until TTL expiry. Confirmed
|
|
14652
|
+
* misses are negatively cached for the same short TTL. */
|
|
14653
|
+
async getSessionById(sessionId) {
|
|
14654
|
+
const now2 = Date.now();
|
|
14655
|
+
if (this.cacheIsFresh(now2)) {
|
|
14656
|
+
const cached = this.cachedSessions.find((session) => session.id === sessionId);
|
|
14657
|
+
if (cached) return cached;
|
|
14658
|
+
const missAt = this.confirmedMisses.get(sessionId);
|
|
14659
|
+
if (missAt !== void 0 && now2 - missAt < INVENTORY_CACHE_TTL_MS) {
|
|
14660
|
+
return void 0;
|
|
14661
|
+
}
|
|
14662
|
+
const refreshed2 = await this.refreshInventory();
|
|
14663
|
+
const found2 = refreshed2.find((session) => session.id === sessionId);
|
|
14664
|
+
if (!found2) this.confirmedMisses.set(sessionId, Date.now());
|
|
14665
|
+
return found2;
|
|
14666
|
+
}
|
|
14667
|
+
const refreshed = await this.refreshInventory();
|
|
14668
|
+
const found = refreshed.find((session) => session.id === sessionId);
|
|
14669
|
+
if (!found) this.confirmedMisses.set(sessionId, Date.now());
|
|
14670
|
+
return found;
|
|
14671
|
+
}
|
|
12382
14672
|
/** Announce to the registry group via WebSocket. */
|
|
12383
14673
|
async announceToRegistry(ws, registryGroup, type = "heartbeat", sessions) {
|
|
12384
14674
|
if (ws.readyState !== import_ws2.default.OPEN || !registryGroup) return;
|
|
12385
|
-
const finalSessions = sessions
|
|
14675
|
+
const finalSessions = sessions ? this.rememberInventory(sessions) : await this.getSessionInventory();
|
|
12386
14676
|
const vitals = this.collectVitals();
|
|
12387
14677
|
sendToGroup(ws, registryGroup, {
|
|
12388
14678
|
type,
|
|
@@ -12400,7 +14690,7 @@ var InventoryManager = class {
|
|
|
12400
14690
|
* and HTTP heartbeat as needed.
|
|
12401
14691
|
*/
|
|
12402
14692
|
async syncInventory(ws, groups, force = false) {
|
|
12403
|
-
const sessions = await
|
|
14693
|
+
const sessions = await this.getSessionInventory(force);
|
|
12404
14694
|
const fingerprint = sessionInventoryFingerprint(sessions);
|
|
12405
14695
|
const changed = fingerprint !== this.lastSentFingerprint;
|
|
12406
14696
|
const checkpointDue = Date.now() - this.lastHttpHeartbeatAt > HTTP_HEARTBEAT_MS;
|
|
@@ -12461,14 +14751,19 @@ var InventoryManager = class {
|
|
|
12461
14751
|
|
|
12462
14752
|
// src/connection-manager.ts
|
|
12463
14753
|
var import_ws3 = __toESM(require("ws"));
|
|
12464
|
-
var
|
|
14754
|
+
var import_node_crypto3 = require("crypto");
|
|
12465
14755
|
var log21 = createLogger("connection");
|
|
12466
14756
|
var OPPORTUNISTIC_INVENTORY_SYNC_MS = 1200;
|
|
12467
14757
|
var MIN_INVENTORY_SYNC_GAP_MS = 1500;
|
|
14758
|
+
var MAX_RX_VIEWERS = 256;
|
|
14759
|
+
var MAX_VIEWER_ID_BYTES = 128;
|
|
14760
|
+
var MAX_BINDING_AGE_MS = 10 * 60 * 1e3;
|
|
14761
|
+
var MAX_BINDING_FUTURE_MS = 2 * 60 * 1e3;
|
|
12468
14762
|
var ConnectionManager = class {
|
|
12469
14763
|
activeSocket = null;
|
|
12470
14764
|
activeGroups = { privateGroup: "", registryGroup: "" };
|
|
12471
14765
|
sessionKey = null;
|
|
14766
|
+
serverClockOffsetMs = 0;
|
|
12472
14767
|
reconnectDelay = DEFAULT_RECONNECT_DELAY_MS;
|
|
12473
14768
|
/** Outbound frame counter; reset on every new handshake. See #310. */
|
|
12474
14769
|
txSeq = new SequenceTracker();
|
|
@@ -12481,7 +14776,7 @@ var ConnectionManager = class {
|
|
|
12481
14776
|
* trackers when this epoch changes, recovering the stream without a manual
|
|
12482
14777
|
* refresh. A new value per ConnectionManager == per CLI process. See #543.
|
|
12483
14778
|
*/
|
|
12484
|
-
streamEpoch = (0,
|
|
14779
|
+
streamEpoch = (0, import_node_crypto3.randomUUID)();
|
|
12485
14780
|
/**
|
|
12486
14781
|
* Inbound frame trackers, keyed by viewerId. The portal's Terminal.tsx
|
|
12487
14782
|
* initializes a fresh txSeq (starting at 1) on every component mount, so a
|
|
@@ -12497,10 +14792,19 @@ var ConnectionManager = class {
|
|
|
12497
14792
|
* the field.
|
|
12498
14793
|
*/
|
|
12499
14794
|
getRxSeqForViewer(viewerId) {
|
|
12500
|
-
const
|
|
12501
|
-
|
|
14795
|
+
const normalized = this.normalizeViewerId(viewerId);
|
|
14796
|
+
if (normalized === null) return null;
|
|
14797
|
+
const isValid = normalized !== "_legacy";
|
|
14798
|
+
const key = normalized;
|
|
12502
14799
|
let tracker = this.rxSeqByViewer.get(key);
|
|
12503
|
-
if (
|
|
14800
|
+
if (tracker) {
|
|
14801
|
+
this.rxSeqByViewer.delete(key);
|
|
14802
|
+
this.rxSeqByViewer.set(key, tracker);
|
|
14803
|
+
} else {
|
|
14804
|
+
if (this.rxSeqByViewer.size >= MAX_RX_VIEWERS) {
|
|
14805
|
+
const oldest = this.rxSeqByViewer.keys().next().value;
|
|
14806
|
+
if (oldest !== void 0) this.rxSeqByViewer.delete(oldest);
|
|
14807
|
+
}
|
|
12504
14808
|
tracker = new SequenceTracker();
|
|
12505
14809
|
this.rxSeqByViewer.set(key, tracker);
|
|
12506
14810
|
if (!isValid) {
|
|
@@ -12509,6 +14813,67 @@ var ConnectionManager = class {
|
|
|
12509
14813
|
}
|
|
12510
14814
|
return tracker;
|
|
12511
14815
|
}
|
|
14816
|
+
/** Authenticate ciphertext before allocating or advancing replay state. */
|
|
14817
|
+
authenticateInboundFrame(enc, iv, seq, viewerId, bindingContext) {
|
|
14818
|
+
if (!this.sessionKey) return { ok: false, reason: "missing_key" };
|
|
14819
|
+
if (!Number.isInteger(seq) || seq < 1 || seq > 4294967295) {
|
|
14820
|
+
return { ok: false, reason: "invalid_seq" };
|
|
14821
|
+
}
|
|
14822
|
+
let binding;
|
|
14823
|
+
let authenticatedViewerKey;
|
|
14824
|
+
const frameBindingVersion = bindingContext?.bindingVersion;
|
|
14825
|
+
if (frameBindingVersion === CRYPTO_BINDING_V2_VERSION) {
|
|
14826
|
+
if (!bindingContext) return { ok: false, reason: "invalid_binding" };
|
|
14827
|
+
if (typeof viewerId !== "string" || typeof bindingContext.sessionId !== "string") {
|
|
14828
|
+
return { ok: false, reason: "invalid_binding" };
|
|
14829
|
+
}
|
|
14830
|
+
binding = {
|
|
14831
|
+
bindingVersion: CRYPTO_BINDING_V2_VERSION,
|
|
14832
|
+
direction: CRYPTO_BINDING_V2_DIRECTION,
|
|
14833
|
+
type: bindingContext.type,
|
|
14834
|
+
action: bindingContext.action,
|
|
14835
|
+
viewerId,
|
|
14836
|
+
sessionId: bindingContext.sessionId,
|
|
14837
|
+
sentAtMs: bindingContext.sentAtMs,
|
|
14838
|
+
seq
|
|
14839
|
+
};
|
|
14840
|
+
try {
|
|
14841
|
+
validateCryptoBindingV2(binding);
|
|
14842
|
+
} catch {
|
|
14843
|
+
return { ok: false, reason: "invalid_binding" };
|
|
14844
|
+
}
|
|
14845
|
+
authenticatedViewerKey = binding.viewerId;
|
|
14846
|
+
const trustedNow = Date.now() + this.serverClockOffsetMs;
|
|
14847
|
+
if (binding.sentAtMs < trustedNow - MAX_BINDING_AGE_MS || binding.sentAtMs > trustedNow + MAX_BINDING_FUTURE_MS) {
|
|
14848
|
+
return { ok: false, reason: "stale_binding" };
|
|
14849
|
+
}
|
|
14850
|
+
} else if (frameBindingVersion !== void 0 && frameBindingVersion !== 1) {
|
|
14851
|
+
return { ok: false, reason: "invalid_binding" };
|
|
14852
|
+
}
|
|
14853
|
+
const plaintext = decryptPayload(enc, iv, this.sessionKey, seq, binding);
|
|
14854
|
+
if (plaintext === null) return { ok: false, reason: "decrypt_failed" };
|
|
14855
|
+
const tracker = this.getRxSeqForViewer(authenticatedViewerKey ?? viewerId);
|
|
14856
|
+
if (!tracker) return { ok: false, reason: "invalid_viewer" };
|
|
14857
|
+
const viewerKey = authenticatedViewerKey ?? this.normalizeViewerId(viewerId);
|
|
14858
|
+
if (!tracker.accept(seq)) {
|
|
14859
|
+
return {
|
|
14860
|
+
ok: false,
|
|
14861
|
+
reason: "replayed",
|
|
14862
|
+
viewerKey,
|
|
14863
|
+
lastSeen: tracker.lastSeq
|
|
14864
|
+
};
|
|
14865
|
+
}
|
|
14866
|
+
return { ok: true, plaintext, viewerKey };
|
|
14867
|
+
}
|
|
14868
|
+
normalizeViewerId(viewerId) {
|
|
14869
|
+
if (viewerId === void 0 || viewerId === null) return "_legacy";
|
|
14870
|
+
if (typeof viewerId !== "string") return "_legacy";
|
|
14871
|
+
if (viewerId.length > MAX_VIEWER_ID_BYTES) return null;
|
|
14872
|
+
const normalized = viewerId.trim();
|
|
14873
|
+
if (normalized.length === 0) return "_legacy";
|
|
14874
|
+
if (Buffer.byteLength(normalized, "utf8") > MAX_VIEWER_ID_BYTES) return null;
|
|
14875
|
+
return normalized;
|
|
14876
|
+
}
|
|
12512
14877
|
wsHeartbeatTimer = null;
|
|
12513
14878
|
inventorySyncTimer = null;
|
|
12514
14879
|
inventorySyncKickTimer = null;
|
|
@@ -12530,7 +14895,7 @@ var ConnectionManager = class {
|
|
|
12530
14895
|
return this.activeSocket !== null && this.activeSocket.readyState === import_ws3.default.OPEN;
|
|
12531
14896
|
}
|
|
12532
14897
|
/** Set the active socket and groups from a negotiate result. */
|
|
12533
|
-
setConnection(ws, groups, sessionKey) {
|
|
14898
|
+
setConnection(ws, groups, sessionKey, crypto4) {
|
|
12534
14899
|
this.activeSocket = ws;
|
|
12535
14900
|
this.activeGroups = groups;
|
|
12536
14901
|
if (sessionKey !== this.sessionKey) {
|
|
@@ -12538,6 +14903,7 @@ var ConnectionManager = class {
|
|
|
12538
14903
|
this.rxSeqByViewer.clear();
|
|
12539
14904
|
}
|
|
12540
14905
|
this.sessionKey = sessionKey;
|
|
14906
|
+
this.serverClockOffsetMs = Number.isFinite(crypto4?.serverClockOffsetMs) ? crypto4.serverClockOffsetMs : 0;
|
|
12541
14907
|
}
|
|
12542
14908
|
/** Reset reconnect delay on successful connection. */
|
|
12543
14909
|
resetReconnectDelay() {
|
|
@@ -13360,6 +15726,7 @@ async function installLatestCliFromNpm() {
|
|
|
13360
15726
|
var log23 = createLogger("control");
|
|
13361
15727
|
var TMUX_RESYNC_DEBOUNCE_MS = 150;
|
|
13362
15728
|
var TMUX_INPUT_COALESCE_MS = 4;
|
|
15729
|
+
var MAX_MARKDOWN_GENERATION_ENTRIES = 2048;
|
|
13363
15730
|
function collectProcessTreePids(rootPid, visited = /* @__PURE__ */ new Set()) {
|
|
13364
15731
|
if (visited.has(rootPid)) return [];
|
|
13365
15732
|
visited.add(rootPid);
|
|
@@ -13403,15 +15770,22 @@ var ControlDispatcher = class {
|
|
|
13403
15770
|
_approvalEnforcer = null;
|
|
13404
15771
|
tmuxResizeState = /* @__PURE__ */ new Map();
|
|
13405
15772
|
tmuxInputBuffers = /* @__PURE__ */ new Map();
|
|
15773
|
+
markdownGlobalGeneration = 0;
|
|
15774
|
+
/** Globally unique within this dispatcher, preventing ABA when a bounded
|
|
15775
|
+
* generation-map entry is evicted and the same viewer later reconnects. */
|
|
15776
|
+
markdownGenerationSequence = 0;
|
|
15777
|
+
markdownSessionGenerations = /* @__PURE__ */ new Map();
|
|
15778
|
+
markdownViewerGenerations = /* @__PURE__ */ new Map();
|
|
13406
15779
|
/** Send V2 delta ops produced by an rAgent-launched native-stream
|
|
13407
15780
|
* consumer. Same shape as TranscriptWatcherManager's `sendOpsFn` so both
|
|
13408
15781
|
* sources funnel through the same WebSocket helper in `agent.ts`. */
|
|
13409
15782
|
sendNativeOps = null;
|
|
15783
|
+
sendNativeSnapshot = null;
|
|
13410
15784
|
/** Set to true when a reconnect was requested (restart-agent, disconnect). */
|
|
13411
15785
|
reconnectRequested = false;
|
|
13412
15786
|
/** Set to false to stop the agent. */
|
|
13413
15787
|
shouldRun = true;
|
|
13414
|
-
constructor(shell, streamer, inventory, connection, options, transcriptWatcher, sendNativeOps) {
|
|
15788
|
+
constructor(shell, streamer, inventory, connection, options, transcriptWatcher, sendNativeOps, sendNativeSnapshot) {
|
|
13415
15789
|
this.shell = shell;
|
|
13416
15790
|
this.streamer = streamer;
|
|
13417
15791
|
this.inventory = inventory;
|
|
@@ -13419,6 +15793,7 @@ var ControlDispatcher = class {
|
|
|
13419
15793
|
this.options = options;
|
|
13420
15794
|
this.transcriptWatcher = transcriptWatcher ?? null;
|
|
13421
15795
|
this.sendNativeOps = sendNativeOps ?? null;
|
|
15796
|
+
this.sendNativeSnapshot = sendNativeSnapshot ?? null;
|
|
13422
15797
|
}
|
|
13423
15798
|
/** Install or replace the native-stream ops callback. Used by the agent
|
|
13424
15799
|
* runner so native ops route through the same encryption/transport path
|
|
@@ -13541,6 +15916,7 @@ var ControlDispatcher = class {
|
|
|
13541
15916
|
return;
|
|
13542
15917
|
case "restart-agent":
|
|
13543
15918
|
case "disconnect":
|
|
15919
|
+
this.invalidateAllMarkdownIntents();
|
|
13544
15920
|
this.reconnectRequested = true;
|
|
13545
15921
|
this.streamer.stopAll();
|
|
13546
15922
|
if (this.connection.activeSocket?.readyState === import_ws4.default.OPEN) {
|
|
@@ -13551,6 +15927,7 @@ var ControlDispatcher = class {
|
|
|
13551
15927
|
await this.updateAgentFromNpm();
|
|
13552
15928
|
return;
|
|
13553
15929
|
case "stop-agent":
|
|
15930
|
+
this.invalidateAllMarkdownIntents();
|
|
13554
15931
|
this.shouldRun = false;
|
|
13555
15932
|
this.streamer.stopAll();
|
|
13556
15933
|
requestStopSelfService();
|
|
@@ -13560,6 +15937,7 @@ var ControlDispatcher = class {
|
|
|
13560
15937
|
return;
|
|
13561
15938
|
case "stop-session":
|
|
13562
15939
|
if (!sessionId) return;
|
|
15940
|
+
this.invalidateMarkdownIntent(sessionId);
|
|
13563
15941
|
if (sessionId.startsWith("pty:")) {
|
|
13564
15942
|
this.shell.restart();
|
|
13565
15943
|
await this.syncInventory();
|
|
@@ -13587,6 +15965,7 @@ var ControlDispatcher = class {
|
|
|
13587
15965
|
return;
|
|
13588
15966
|
case "kill-process":
|
|
13589
15967
|
if (!sessionId) return;
|
|
15968
|
+
this.invalidateMarkdownIntent(sessionId);
|
|
13590
15969
|
{
|
|
13591
15970
|
const pid = parseProcessPid2(sessionId);
|
|
13592
15971
|
if (pid === null) {
|
|
@@ -13633,9 +16012,10 @@ var ControlDispatcher = class {
|
|
|
13633
16012
|
return;
|
|
13634
16013
|
case "stop-detached": {
|
|
13635
16014
|
if (this.transcriptWatcher) {
|
|
13636
|
-
const sessions = await
|
|
16015
|
+
const sessions = await this.inventory.getSessionInventory(true);
|
|
13637
16016
|
for (const session of sessions) {
|
|
13638
16017
|
if (session.type === "tmux" && session.status === "detached") {
|
|
16018
|
+
this.invalidateMarkdownIntent(session.id);
|
|
13639
16019
|
this.transcriptWatcher.disableMarkdown(session.id);
|
|
13640
16020
|
}
|
|
13641
16021
|
}
|
|
@@ -13657,17 +16037,35 @@ var ControlDispatcher = class {
|
|
|
13657
16037
|
return;
|
|
13658
16038
|
case "stop-stream":
|
|
13659
16039
|
if (sessionId) {
|
|
16040
|
+
this.invalidateMarkdownIntent(sessionId, viewerId ?? void 0);
|
|
13660
16041
|
this.streamer.stopStream(sessionId, viewerId ?? void 0);
|
|
13661
|
-
this.transcriptWatcher?.disableMarkdown(sessionId);
|
|
16042
|
+
if (viewerId) this.transcriptWatcher?.disableMarkdown(sessionId, viewerId);
|
|
16043
|
+
else this.transcriptWatcher?.disableMarkdown(sessionId);
|
|
13662
16044
|
}
|
|
13663
16045
|
return;
|
|
13664
16046
|
case "prefer-markdown":
|
|
13665
|
-
this.handlePreferMarkdown(sessionId, payload);
|
|
16047
|
+
await this.handlePreferMarkdown(sessionId, payload);
|
|
13666
16048
|
return;
|
|
13667
16049
|
case "sync-markdown":
|
|
16050
|
+
if (sessionId && isNativeSession(sessionId)) {
|
|
16051
|
+
if (this.sendNativeSnapshot) {
|
|
16052
|
+
syncNativeTranscriptSnapshot(sessionId, this.sendNativeSnapshot);
|
|
16053
|
+
}
|
|
16054
|
+
return;
|
|
16055
|
+
}
|
|
13668
16056
|
if (sessionId && this.transcriptWatcher) {
|
|
13669
16057
|
const fromSeq = typeof payload.fromSeq === "number" ? payload.fromSeq : void 0;
|
|
13670
|
-
|
|
16058
|
+
const schemaVersion = payload.schemaVersion === 1 || payload.schemaVersion === 2 ? payload.schemaVersion : void 0;
|
|
16059
|
+
if (schemaVersion !== void 0 || viewerId) {
|
|
16060
|
+
this.transcriptWatcher.handleSyncRequest(
|
|
16061
|
+
sessionId,
|
|
16062
|
+
fromSeq,
|
|
16063
|
+
schemaVersion,
|
|
16064
|
+
viewerId ?? void 0
|
|
16065
|
+
);
|
|
16066
|
+
} else {
|
|
16067
|
+
this.transcriptWatcher.handleSyncRequest(sessionId, fromSeq);
|
|
16068
|
+
}
|
|
13671
16069
|
}
|
|
13672
16070
|
return;
|
|
13673
16071
|
case "upload-file":
|
|
@@ -13822,11 +16220,25 @@ var ControlDispatcher = class {
|
|
|
13822
16220
|
});
|
|
13823
16221
|
}
|
|
13824
16222
|
}
|
|
13825
|
-
handlePreferMarkdown(sessionId, payload) {
|
|
16223
|
+
async handlePreferMarkdown(sessionId, payload) {
|
|
13826
16224
|
if (!sessionId || !this.transcriptWatcher) return;
|
|
13827
16225
|
const enabled = payload.enabled === true;
|
|
16226
|
+
const viewerId = typeof payload.viewerId === "string" && payload.viewerId.trim() ? payload.viewerId.trim() : void 0;
|
|
16227
|
+
const schemaVersion = payload.schemaVersion === 1 || payload.schemaVersion === 2 ? payload.schemaVersion : void 0;
|
|
16228
|
+
if (!enabled) {
|
|
16229
|
+
this.invalidateMarkdownIntent(sessionId, viewerId);
|
|
16230
|
+
if (!isNativeSession(sessionId)) {
|
|
16231
|
+
if (viewerId) this.transcriptWatcher.disableMarkdown(sessionId, viewerId);
|
|
16232
|
+
else this.transcriptWatcher.disableMarkdown(sessionId);
|
|
16233
|
+
}
|
|
16234
|
+
return;
|
|
16235
|
+
}
|
|
16236
|
+
const intent = this.beginMarkdownIntent(sessionId, viewerId);
|
|
13828
16237
|
if (isNativeSession(sessionId)) {
|
|
13829
16238
|
if (enabled) {
|
|
16239
|
+
if (this.sendNativeSnapshot) {
|
|
16240
|
+
syncNativeTranscriptSnapshot(sessionId, this.sendNativeSnapshot);
|
|
16241
|
+
}
|
|
13830
16242
|
this.sendMarkdownStatus({
|
|
13831
16243
|
type: "markdown",
|
|
13832
16244
|
subtype: "status",
|
|
@@ -13837,8 +16249,33 @@ var ControlDispatcher = class {
|
|
|
13837
16249
|
return;
|
|
13838
16250
|
}
|
|
13839
16251
|
if (enabled) {
|
|
13840
|
-
const
|
|
13841
|
-
|
|
16252
|
+
const requestedAgentType = typeof payload.agentType === "string" ? payload.agentType : void 0;
|
|
16253
|
+
let agentType = requestedAgentType;
|
|
16254
|
+
try {
|
|
16255
|
+
const liveAgentType = (await this.inventory.getSessionById(sessionId))?.agentType;
|
|
16256
|
+
if (liveAgentType) {
|
|
16257
|
+
agentType = liveAgentType;
|
|
16258
|
+
if (requestedAgentType && requestedAgentType !== liveAgentType) {
|
|
16259
|
+
log23.info("corrected stale transcript agent type from live inventory", {
|
|
16260
|
+
sessionId,
|
|
16261
|
+
requestedAgentType,
|
|
16262
|
+
liveAgentType
|
|
16263
|
+
});
|
|
16264
|
+
}
|
|
16265
|
+
}
|
|
16266
|
+
} catch (error) {
|
|
16267
|
+
log23.warn("could not resolve live transcript agent type", {
|
|
16268
|
+
sessionId,
|
|
16269
|
+
error: error instanceof Error ? error.message : String(error)
|
|
16270
|
+
});
|
|
16271
|
+
}
|
|
16272
|
+
if (!this.isCurrentMarkdownIntent(intent)) return;
|
|
16273
|
+
const result = schemaVersion !== void 0 || viewerId ? this.transcriptWatcher.enableMarkdown(
|
|
16274
|
+
sessionId,
|
|
16275
|
+
agentType,
|
|
16276
|
+
viewerId,
|
|
16277
|
+
schemaVersion
|
|
16278
|
+
) : this.transcriptWatcher.enableMarkdown(sessionId, agentType);
|
|
13842
16279
|
if (!result.ok) {
|
|
13843
16280
|
this.sendMarkdownStatus({
|
|
13844
16281
|
type: "markdown",
|
|
@@ -13856,10 +16293,77 @@ var ControlDispatcher = class {
|
|
|
13856
16293
|
status: "enabled"
|
|
13857
16294
|
});
|
|
13858
16295
|
}
|
|
13859
|
-
} else {
|
|
13860
|
-
this.transcriptWatcher.disableMarkdown(sessionId);
|
|
13861
16296
|
}
|
|
13862
16297
|
}
|
|
16298
|
+
markdownViewerKey(sessionId, viewerId) {
|
|
16299
|
+
return `${sessionId}\0${viewerId || "__legacy_anonymous__"}`;
|
|
16300
|
+
}
|
|
16301
|
+
beginMarkdownIntent(sessionId, viewerId) {
|
|
16302
|
+
const viewerKey = this.markdownViewerKey(sessionId, viewerId);
|
|
16303
|
+
let sessionGeneration = this.markdownSessionGenerations.get(sessionId);
|
|
16304
|
+
if (sessionGeneration === void 0) {
|
|
16305
|
+
sessionGeneration = this.nextMarkdownGeneration();
|
|
16306
|
+
}
|
|
16307
|
+
this.setBoundedMarkdownGeneration(
|
|
16308
|
+
this.markdownSessionGenerations,
|
|
16309
|
+
sessionId,
|
|
16310
|
+
sessionGeneration
|
|
16311
|
+
);
|
|
16312
|
+
const viewerGeneration = this.nextMarkdownGeneration();
|
|
16313
|
+
this.setBoundedMarkdownGeneration(
|
|
16314
|
+
this.markdownViewerGenerations,
|
|
16315
|
+
viewerKey,
|
|
16316
|
+
viewerGeneration
|
|
16317
|
+
);
|
|
16318
|
+
return {
|
|
16319
|
+
globalGeneration: this.markdownGlobalGeneration,
|
|
16320
|
+
sessionGeneration,
|
|
16321
|
+
viewerGeneration,
|
|
16322
|
+
sessionId,
|
|
16323
|
+
viewerKey
|
|
16324
|
+
};
|
|
16325
|
+
}
|
|
16326
|
+
invalidateMarkdownIntent(sessionId, viewerId) {
|
|
16327
|
+
if (viewerId) {
|
|
16328
|
+
const viewerKey = this.markdownViewerKey(sessionId, viewerId);
|
|
16329
|
+
this.setBoundedMarkdownGeneration(
|
|
16330
|
+
this.markdownViewerGenerations,
|
|
16331
|
+
viewerKey,
|
|
16332
|
+
this.nextMarkdownGeneration()
|
|
16333
|
+
);
|
|
16334
|
+
return;
|
|
16335
|
+
}
|
|
16336
|
+
this.setBoundedMarkdownGeneration(
|
|
16337
|
+
this.markdownSessionGenerations,
|
|
16338
|
+
sessionId,
|
|
16339
|
+
this.nextMarkdownGeneration()
|
|
16340
|
+
);
|
|
16341
|
+
const viewerPrefix = `${sessionId}\0`;
|
|
16342
|
+
for (const key of this.markdownViewerGenerations.keys()) {
|
|
16343
|
+
if (key.startsWith(viewerPrefix)) this.markdownViewerGenerations.delete(key);
|
|
16344
|
+
}
|
|
16345
|
+
}
|
|
16346
|
+
nextMarkdownGeneration() {
|
|
16347
|
+
this.markdownGenerationSequence++;
|
|
16348
|
+
return this.markdownGenerationSequence;
|
|
16349
|
+
}
|
|
16350
|
+
setBoundedMarkdownGeneration(generations, key, generation) {
|
|
16351
|
+
generations.delete(key);
|
|
16352
|
+
generations.set(key, generation);
|
|
16353
|
+
while (generations.size > MAX_MARKDOWN_GENERATION_ENTRIES) {
|
|
16354
|
+
const oldest = generations.keys().next();
|
|
16355
|
+
if (oldest.done) break;
|
|
16356
|
+
generations.delete(oldest.value);
|
|
16357
|
+
}
|
|
16358
|
+
}
|
|
16359
|
+
invalidateAllMarkdownIntents() {
|
|
16360
|
+
this.markdownGlobalGeneration++;
|
|
16361
|
+
this.markdownSessionGenerations.clear();
|
|
16362
|
+
this.markdownViewerGenerations.clear();
|
|
16363
|
+
}
|
|
16364
|
+
isCurrentMarkdownIntent(intent) {
|
|
16365
|
+
return intent.globalGeneration === this.markdownGlobalGeneration && intent.sessionGeneration === (this.markdownSessionGenerations.get(intent.sessionId) ?? 0) && intent.viewerGeneration === (this.markdownViewerGenerations.get(intent.viewerKey) ?? 0);
|
|
16366
|
+
}
|
|
13863
16367
|
sendMarkdownStatus(payload) {
|
|
13864
16368
|
const ws = this.connection.activeSocket;
|
|
13865
16369
|
const group = this.connection.activeGroups.privateGroup;
|
|
@@ -13868,6 +16372,7 @@ var ControlDispatcher = class {
|
|
|
13868
16372
|
}
|
|
13869
16373
|
/** Stop all transcript watchers (called on disconnect/cleanup). */
|
|
13870
16374
|
stopTranscriptWatchers() {
|
|
16375
|
+
this.invalidateAllMarkdownIntents();
|
|
13871
16376
|
this.transcriptWatcher?.stopAll();
|
|
13872
16377
|
}
|
|
13873
16378
|
async syncInventory(force = false) {
|
|
@@ -14167,7 +16672,7 @@ function validateProvisionRequest(payload) {
|
|
|
14167
16672
|
}
|
|
14168
16673
|
|
|
14169
16674
|
// src/approval-policy.ts
|
|
14170
|
-
var
|
|
16675
|
+
var import_node_crypto4 = require("crypto");
|
|
14171
16676
|
function hasToken(cmd, token) {
|
|
14172
16677
|
const lower = cmd.toLowerCase();
|
|
14173
16678
|
const t = token.toLowerCase();
|
|
@@ -14274,7 +16779,7 @@ function matchPolicy(command, _policy = DEFAULT_APPROVAL_POLICY, hostId = "", se
|
|
|
14274
16779
|
for (const matcher of MATCHERS) {
|
|
14275
16780
|
if (matcher.test(command)) {
|
|
14276
16781
|
return {
|
|
14277
|
-
requestId: (0,
|
|
16782
|
+
requestId: (0, import_node_crypto4.randomUUID)(),
|
|
14278
16783
|
hostId,
|
|
14279
16784
|
sessionId,
|
|
14280
16785
|
action: matcher.action,
|
|
@@ -14577,6 +17082,65 @@ var ApprovalEnforcer = class {
|
|
|
14577
17082
|
}
|
|
14578
17083
|
};
|
|
14579
17084
|
|
|
17085
|
+
// src/native-transcript-recovery.ts
|
|
17086
|
+
var NATIVE_TRANSCRIPT_RECOVERY_DRAIN_MS = 300;
|
|
17087
|
+
var NativeTranscriptRecoveryDrain = class {
|
|
17088
|
+
constructor(options) {
|
|
17089
|
+
this.options = options;
|
|
17090
|
+
}
|
|
17091
|
+
options;
|
|
17092
|
+
pending = /* @__PURE__ */ new Set();
|
|
17093
|
+
timer = null;
|
|
17094
|
+
stopped = false;
|
|
17095
|
+
/** Merge the current authoritative session IDs into the pending work set. */
|
|
17096
|
+
request() {
|
|
17097
|
+
if (this.stopped) return;
|
|
17098
|
+
for (const sessionId of this.options.listSessionIds()) {
|
|
17099
|
+
if (sessionId.length > 0) this.pending.add(sessionId);
|
|
17100
|
+
}
|
|
17101
|
+
this.schedule();
|
|
17102
|
+
}
|
|
17103
|
+
/** Permanently stop the drain and release all queued recovery work. */
|
|
17104
|
+
cancel() {
|
|
17105
|
+
this.stopped = true;
|
|
17106
|
+
this.pending.clear();
|
|
17107
|
+
if (this.timer) {
|
|
17108
|
+
clearTimeout(this.timer);
|
|
17109
|
+
this.timer = null;
|
|
17110
|
+
}
|
|
17111
|
+
}
|
|
17112
|
+
schedule() {
|
|
17113
|
+
if (this.stopped || this.timer || this.pending.size === 0) return;
|
|
17114
|
+
this.timer = setTimeout(
|
|
17115
|
+
() => {
|
|
17116
|
+
this.timer = null;
|
|
17117
|
+
this.drainOne();
|
|
17118
|
+
},
|
|
17119
|
+
this.options.intervalMs ?? NATIVE_TRANSCRIPT_RECOVERY_DRAIN_MS
|
|
17120
|
+
);
|
|
17121
|
+
if (typeof this.timer.unref === "function") this.timer.unref();
|
|
17122
|
+
}
|
|
17123
|
+
drainOne() {
|
|
17124
|
+
if (this.stopped || this.pending.size === 0) return;
|
|
17125
|
+
if (this.options.canSend?.() === false || this.options.isQueueHigh()) {
|
|
17126
|
+
this.schedule();
|
|
17127
|
+
return;
|
|
17128
|
+
}
|
|
17129
|
+
const next = this.pending.values().next();
|
|
17130
|
+
if (next.done) return;
|
|
17131
|
+
const sessionId = next.value;
|
|
17132
|
+
try {
|
|
17133
|
+
this.options.syncSession(sessionId);
|
|
17134
|
+
this.pending.delete(sessionId);
|
|
17135
|
+
} catch (error) {
|
|
17136
|
+
this.pending.delete(sessionId);
|
|
17137
|
+
this.pending.add(sessionId);
|
|
17138
|
+
this.options.onError?.(sessionId, error);
|
|
17139
|
+
}
|
|
17140
|
+
this.schedule();
|
|
17141
|
+
}
|
|
17142
|
+
};
|
|
17143
|
+
|
|
14580
17144
|
// src/agent.ts
|
|
14581
17145
|
var log25 = createLogger("agent");
|
|
14582
17146
|
var MAX_TRANSPORT_CHUNK_BYTES = 24 * 1024;
|
|
@@ -14654,6 +17218,26 @@ function splitTransportChunks(data, maxBytes = MAX_TRANSPORT_CHUNK_BYTES) {
|
|
|
14654
17218
|
}
|
|
14655
17219
|
return chunks;
|
|
14656
17220
|
}
|
|
17221
|
+
function parseAuthenticatedUploadControlPayload(plaintext, outer) {
|
|
17222
|
+
let parsed;
|
|
17223
|
+
try {
|
|
17224
|
+
parsed = JSON.parse(plaintext);
|
|
17225
|
+
} catch {
|
|
17226
|
+
return null;
|
|
17227
|
+
}
|
|
17228
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
17229
|
+
const inner = parsed;
|
|
17230
|
+
if (outer.action !== "upload-file" || inner.action !== outer.action || inner.sessionId !== outer.sessionId || inner.viewerId !== outer.viewerId) {
|
|
17231
|
+
return null;
|
|
17232
|
+
}
|
|
17233
|
+
return {
|
|
17234
|
+
...inner,
|
|
17235
|
+
action: outer.action,
|
|
17236
|
+
sessionId: outer.sessionId,
|
|
17237
|
+
viewerId: outer.viewerId,
|
|
17238
|
+
_verifiedEncrypted: true
|
|
17239
|
+
};
|
|
17240
|
+
}
|
|
14657
17241
|
async function runAgent(rawOptions) {
|
|
14658
17242
|
const options = resolveRunOptions(rawOptions);
|
|
14659
17243
|
if (!options.agentToken) {
|
|
@@ -14677,6 +17261,7 @@ async function runAgent(rawOptions) {
|
|
|
14677
17261
|
}
|
|
14678
17262
|
const outputBuffer = new OutputBuffer();
|
|
14679
17263
|
const ptySessionId = `pty:${options.hostId}`;
|
|
17264
|
+
let nativeTranscriptRecovery = null;
|
|
14680
17265
|
const sessionStreamer = new SessionStreamer(
|
|
14681
17266
|
(sessionId, data, viewerId) => {
|
|
14682
17267
|
if (!conn.isReady()) {
|
|
@@ -14727,6 +17312,7 @@ async function runAgent(rawOptions) {
|
|
|
14727
17312
|
},
|
|
14728
17313
|
() => {
|
|
14729
17314
|
transcriptWatcher.resyncMarkdownAfterDrop();
|
|
17315
|
+
nativeTranscriptRecovery?.request();
|
|
14730
17316
|
},
|
|
14731
17317
|
// #556 follow-up: lets the streamer gate resync delivery + the pipe-liveness
|
|
14732
17318
|
// watchdog on a confirmed-OPEN socket, so resync bytes aren't buffered then
|
|
@@ -14749,41 +17335,98 @@ async function runAgent(rawOptions) {
|
|
|
14749
17335
|
}
|
|
14750
17336
|
}
|
|
14751
17337
|
};
|
|
17338
|
+
const sendTranscriptSnapshot = (sessionId, snapshot, seq, reset) => {
|
|
17339
|
+
if (!conn.isReady()) return;
|
|
17340
|
+
const safeSnapshot = trimSnapshot(snapshot);
|
|
17341
|
+
const payload = {
|
|
17342
|
+
type: "markdown",
|
|
17343
|
+
subtype: "snapshot",
|
|
17344
|
+
schemaVersion: 2,
|
|
17345
|
+
sessionId,
|
|
17346
|
+
seq,
|
|
17347
|
+
snapshot: safeSnapshot,
|
|
17348
|
+
...reset ? { reset: true } : {}
|
|
17349
|
+
};
|
|
17350
|
+
if (conn.sessionKey) {
|
|
17351
|
+
const contentStr = JSON.stringify(payload);
|
|
17352
|
+
const { enc, iv, seq: frameSeq } = encryptPayload(
|
|
17353
|
+
contentStr,
|
|
17354
|
+
conn.sessionKey,
|
|
17355
|
+
conn.txSeq.next()
|
|
17356
|
+
);
|
|
17357
|
+
sendToGroup(
|
|
17358
|
+
conn.activeSocket,
|
|
17359
|
+
conn.activeGroups.privateGroup,
|
|
17360
|
+
{ type: "markdown", enc, iv, seq: frameSeq, sessionId },
|
|
17361
|
+
{ markdownSubtype: "snapshot", markdownSchemaVersion: 2, sessionId, reset }
|
|
17362
|
+
);
|
|
17363
|
+
} else {
|
|
17364
|
+
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
|
|
17365
|
+
}
|
|
17366
|
+
};
|
|
17367
|
+
nativeTranscriptRecovery = new NativeTranscriptRecoveryDrain({
|
|
17368
|
+
listSessionIds: getNativeTranscriptSnapshotSessionIds,
|
|
17369
|
+
syncSession: (sessionId) => syncNativeTranscriptSnapshot(
|
|
17370
|
+
sessionId,
|
|
17371
|
+
sendTranscriptSnapshot,
|
|
17372
|
+
true
|
|
17373
|
+
),
|
|
17374
|
+
isQueueHigh: () => getQueuePressure().isHigh,
|
|
17375
|
+
canSend: () => conn.isReady(),
|
|
17376
|
+
onError: (sessionId, error) => {
|
|
17377
|
+
log25.warn("native transcript recovery send failed; retrying", {
|
|
17378
|
+
sessionId,
|
|
17379
|
+
error: error instanceof Error ? error.message : String(error)
|
|
17380
|
+
});
|
|
17381
|
+
}
|
|
17382
|
+
});
|
|
14752
17383
|
const transcriptWatcher = new TranscriptWatcherManager(
|
|
14753
17384
|
(sessionId, turn, seq) => {
|
|
14754
17385
|
if (!conn.isReady()) return;
|
|
17386
|
+
const safeTurn = boundMarkdownTurnForWire(turn);
|
|
14755
17387
|
const payload = {
|
|
14756
17388
|
type: "markdown",
|
|
14757
17389
|
subtype: "delta",
|
|
14758
17390
|
sessionId,
|
|
14759
17391
|
seq,
|
|
14760
|
-
turnId:
|
|
14761
|
-
role:
|
|
14762
|
-
content:
|
|
14763
|
-
tools:
|
|
14764
|
-
timestamp:
|
|
17392
|
+
turnId: safeTurn.turnId,
|
|
17393
|
+
role: safeTurn.role,
|
|
17394
|
+
content: safeTurn.content,
|
|
17395
|
+
tools: safeTurn.tools,
|
|
17396
|
+
timestamp: safeTurn.timestamp
|
|
14765
17397
|
};
|
|
14766
17398
|
if (conn.sessionKey) {
|
|
14767
17399
|
const contentStr = JSON.stringify(payload);
|
|
14768
17400
|
const { enc, iv, seq: frameSeq } = encryptPayload(contentStr, conn.sessionKey, conn.txSeq.next());
|
|
14769
|
-
sendToGroup(
|
|
17401
|
+
sendToGroup(
|
|
17402
|
+
conn.activeSocket,
|
|
17403
|
+
conn.activeGroups.privateGroup,
|
|
17404
|
+
{ type: "markdown", enc, iv, seq: frameSeq, sessionId },
|
|
17405
|
+
{ markdownSubtype: "delta", markdownSchemaVersion: 1, sessionId }
|
|
17406
|
+
);
|
|
14770
17407
|
} else {
|
|
14771
17408
|
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
|
|
14772
17409
|
}
|
|
14773
17410
|
},
|
|
14774
17411
|
(sessionId, turns, seq) => {
|
|
14775
17412
|
if (!conn.isReady()) return;
|
|
17413
|
+
const safeTurns = boundMarkdownTurnsForWire(turns);
|
|
14776
17414
|
const payload = {
|
|
14777
17415
|
type: "markdown",
|
|
14778
17416
|
subtype: "snapshot",
|
|
14779
17417
|
sessionId,
|
|
14780
17418
|
seq,
|
|
14781
|
-
turns
|
|
17419
|
+
turns: safeTurns
|
|
14782
17420
|
};
|
|
14783
17421
|
if (conn.sessionKey) {
|
|
14784
17422
|
const contentStr = JSON.stringify(payload);
|
|
14785
17423
|
const { enc, iv, seq: frameSeq } = encryptPayload(contentStr, conn.sessionKey, conn.txSeq.next());
|
|
14786
|
-
sendToGroup(
|
|
17424
|
+
sendToGroup(
|
|
17425
|
+
conn.activeSocket,
|
|
17426
|
+
conn.activeGroups.privateGroup,
|
|
17427
|
+
{ type: "markdown", enc, iv, seq: frameSeq, sessionId },
|
|
17428
|
+
{ markdownSubtype: "snapshot", markdownSchemaVersion: 1, sessionId }
|
|
17429
|
+
);
|
|
14787
17430
|
} else {
|
|
14788
17431
|
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
|
|
14789
17432
|
}
|
|
@@ -14791,45 +17434,30 @@ async function runAgent(rawOptions) {
|
|
|
14791
17434
|
// V2: send block-level delta ops
|
|
14792
17435
|
(sessionId, ops, seq) => {
|
|
14793
17436
|
if (!conn.isReady()) return;
|
|
14794
|
-
const
|
|
14795
|
-
|
|
14796
|
-
|
|
14797
|
-
|
|
14798
|
-
|
|
14799
|
-
|
|
14800
|
-
|
|
14801
|
-
|
|
14802
|
-
|
|
14803
|
-
|
|
14804
|
-
|
|
14805
|
-
|
|
14806
|
-
|
|
14807
|
-
|
|
14808
|
-
|
|
14809
|
-
|
|
14810
|
-
|
|
14811
|
-
|
|
14812
|
-
|
|
14813
|
-
|
|
14814
|
-
|
|
14815
|
-
if (!conn.isReady()) return;
|
|
14816
|
-
const payload = {
|
|
14817
|
-
type: "markdown",
|
|
14818
|
-
subtype: "snapshot",
|
|
14819
|
-
schemaVersion: 2,
|
|
14820
|
-
sessionId,
|
|
14821
|
-
seq,
|
|
14822
|
-
snapshot,
|
|
14823
|
-
...reset ? { reset: true } : {}
|
|
14824
|
-
};
|
|
14825
|
-
if (conn.sessionKey) {
|
|
14826
|
-
const contentStr = JSON.stringify(payload);
|
|
14827
|
-
const { enc, iv, seq: frameSeq } = encryptPayload(contentStr, conn.sessionKey, conn.txSeq.next());
|
|
14828
|
-
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, seq: frameSeq, sessionId });
|
|
14829
|
-
} else {
|
|
14830
|
-
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
|
|
17437
|
+
for (const safeOps of boundTranscriptOpsForWire(ops)) {
|
|
17438
|
+
const payload = {
|
|
17439
|
+
type: "markdown",
|
|
17440
|
+
subtype: "delta",
|
|
17441
|
+
schemaVersion: 2,
|
|
17442
|
+
sessionId,
|
|
17443
|
+
seq,
|
|
17444
|
+
ops: safeOps
|
|
17445
|
+
};
|
|
17446
|
+
if (conn.sessionKey) {
|
|
17447
|
+
const contentStr = JSON.stringify(payload);
|
|
17448
|
+
const { enc, iv, seq: frameSeq } = encryptPayload(contentStr, conn.sessionKey, conn.txSeq.next());
|
|
17449
|
+
sendToGroup(
|
|
17450
|
+
conn.activeSocket,
|
|
17451
|
+
conn.activeGroups.privateGroup,
|
|
17452
|
+
{ type: "markdown", enc, iv, seq: frameSeq, sessionId },
|
|
17453
|
+
{ markdownSubtype: "delta", markdownSchemaVersion: 2, sessionId }
|
|
17454
|
+
);
|
|
17455
|
+
} else {
|
|
17456
|
+
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
|
|
17457
|
+
}
|
|
14831
17458
|
}
|
|
14832
17459
|
},
|
|
17460
|
+
sendTranscriptSnapshot,
|
|
14833
17461
|
// #483: when a deferred enableMarkdown retry succeeds, flip the portal
|
|
14834
17462
|
// status from "unavailable" back to "enabled" so the markdown view
|
|
14835
17463
|
// drops the loading placeholder without a user-driven reconnect.
|
|
@@ -14846,30 +17474,36 @@ async function runAgent(rawOptions) {
|
|
|
14846
17474
|
);
|
|
14847
17475
|
const sendNativeOps = (sessionId, ops, seq) => {
|
|
14848
17476
|
if (!conn.isReady()) return;
|
|
14849
|
-
const
|
|
14850
|
-
|
|
14851
|
-
subtype: "delta",
|
|
14852
|
-
schemaVersion: 2,
|
|
14853
|
-
sessionId,
|
|
14854
|
-
seq,
|
|
14855
|
-
ops
|
|
14856
|
-
};
|
|
14857
|
-
if (conn.sessionKey) {
|
|
14858
|
-
const contentStr = JSON.stringify(payload);
|
|
14859
|
-
const { enc, iv, seq: frameSeq } = encryptPayload(
|
|
14860
|
-
contentStr,
|
|
14861
|
-
conn.sessionKey,
|
|
14862
|
-
conn.txSeq.next()
|
|
14863
|
-
);
|
|
14864
|
-
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, {
|
|
17477
|
+
for (const safeOps of boundTranscriptOpsForWire(ops)) {
|
|
17478
|
+
const payload = {
|
|
14865
17479
|
type: "markdown",
|
|
14866
|
-
|
|
14867
|
-
|
|
14868
|
-
|
|
14869
|
-
|
|
14870
|
-
|
|
14871
|
-
|
|
14872
|
-
|
|
17480
|
+
subtype: "delta",
|
|
17481
|
+
schemaVersion: 2,
|
|
17482
|
+
sessionId,
|
|
17483
|
+
seq,
|
|
17484
|
+
ops: safeOps
|
|
17485
|
+
};
|
|
17486
|
+
if (conn.sessionKey) {
|
|
17487
|
+
const contentStr = JSON.stringify(payload);
|
|
17488
|
+
const { enc, iv, seq: frameSeq } = encryptPayload(
|
|
17489
|
+
contentStr,
|
|
17490
|
+
conn.sessionKey,
|
|
17491
|
+
conn.txSeq.next()
|
|
17492
|
+
);
|
|
17493
|
+
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, {
|
|
17494
|
+
type: "markdown",
|
|
17495
|
+
enc,
|
|
17496
|
+
iv,
|
|
17497
|
+
seq: frameSeq,
|
|
17498
|
+
sessionId
|
|
17499
|
+
}, {
|
|
17500
|
+
markdownSubtype: "delta",
|
|
17501
|
+
markdownSchemaVersion: 2,
|
|
17502
|
+
sessionId
|
|
17503
|
+
});
|
|
17504
|
+
} else {
|
|
17505
|
+
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
|
|
17506
|
+
}
|
|
14873
17507
|
}
|
|
14874
17508
|
};
|
|
14875
17509
|
const shell = new ShellManager(options.command, sendOutput);
|
|
@@ -14881,12 +17515,14 @@ async function runAgent(rawOptions) {
|
|
|
14881
17515
|
conn,
|
|
14882
17516
|
options,
|
|
14883
17517
|
transcriptWatcher,
|
|
14884
|
-
sendNativeOps
|
|
17518
|
+
sendNativeOps,
|
|
17519
|
+
sendTranscriptSnapshot
|
|
14885
17520
|
);
|
|
14886
17521
|
shell.spawn();
|
|
14887
17522
|
const onSignal = () => {
|
|
14888
17523
|
dispatcher.shouldRun = false;
|
|
14889
17524
|
dispatcher.stopTranscriptWatchers();
|
|
17525
|
+
nativeTranscriptRecovery?.cancel();
|
|
14890
17526
|
stopAllNativeSessions();
|
|
14891
17527
|
conn.cleanup({ stopStreams: true });
|
|
14892
17528
|
shell.stop();
|
|
@@ -14914,7 +17550,10 @@ async function runAgent(rawOptions) {
|
|
|
14914
17550
|
};
|
|
14915
17551
|
await new Promise((resolve) => {
|
|
14916
17552
|
const ws = new import_ws5.default(negotiated.url, "json.webpubsub.azure.v1");
|
|
14917
|
-
conn.setConnection(ws, groups, negotiated.sessionKey ?? null
|
|
17553
|
+
conn.setConnection(ws, groups, negotiated.sessionKey ?? null, {
|
|
17554
|
+
cryptoBindingVersion: negotiated.cryptoBindingVersion,
|
|
17555
|
+
serverClockOffsetMs: negotiated.serverClockOffsetMs
|
|
17556
|
+
});
|
|
14918
17557
|
let connectedAt = 0;
|
|
14919
17558
|
if (negotiated.sessionKey) {
|
|
14920
17559
|
const enforcer = new ApprovalEnforcer({
|
|
@@ -14949,6 +17588,7 @@ async function runAgent(rawOptions) {
|
|
|
14949
17588
|
// sequence trackers so restarted frames aren't dropped forever.
|
|
14950
17589
|
streamEpoch: conn.streamEpoch
|
|
14951
17590
|
});
|
|
17591
|
+
nativeTranscriptRecovery?.request();
|
|
14952
17592
|
conn.replayBufferedOutput((chunk) => {
|
|
14953
17593
|
for (const outputChunk of splitTransportChunks(chunk)) {
|
|
14954
17594
|
if (conn.sessionKey) {
|
|
@@ -15003,23 +17643,33 @@ async function runAgent(rawOptions) {
|
|
|
15003
17643
|
let inputData = null;
|
|
15004
17644
|
if (typeof payload.enc === "string" && typeof payload.iv === "string" && conn.sessionKey) {
|
|
15005
17645
|
const inSeq = typeof payload.seq === "number" ? payload.seq : null;
|
|
15006
|
-
const rxSeq = conn.getRxSeqForViewer(payload.viewerId);
|
|
15007
17646
|
if (inSeq === null) {
|
|
15008
17647
|
log25.warn("rejecting encrypted input frame without seq (#310)");
|
|
15009
|
-
} else if (!rxSeq.accept(inSeq)) {
|
|
15010
|
-
log25.warn("rejecting replayed/out-of-order input", {
|
|
15011
|
-
seq: inSeq,
|
|
15012
|
-
lastSeen: rxSeq.lastSeq,
|
|
15013
|
-
viewerId: typeof payload.viewerId === "string" ? payload.viewerId : "_legacy"
|
|
15014
|
-
});
|
|
15015
17648
|
} else {
|
|
15016
|
-
|
|
17649
|
+
const authenticated = conn.authenticateInboundFrame(
|
|
15017
17650
|
payload.enc,
|
|
15018
17651
|
payload.iv,
|
|
15019
|
-
|
|
15020
|
-
|
|
17652
|
+
inSeq,
|
|
17653
|
+
payload.viewerId,
|
|
17654
|
+
{
|
|
17655
|
+
bindingVersion: payload.bindingVersion,
|
|
17656
|
+
sentAtMs: payload.sentAtMs,
|
|
17657
|
+
type: "input",
|
|
17658
|
+
action: "",
|
|
17659
|
+
sessionId: payload.sessionId
|
|
17660
|
+
}
|
|
15021
17661
|
);
|
|
15022
|
-
if (
|
|
17662
|
+
if (authenticated.ok) {
|
|
17663
|
+
inputData = authenticated.plaintext;
|
|
17664
|
+
} else if (authenticated.reason === "replayed") {
|
|
17665
|
+
log25.warn("rejecting replayed/out-of-order input", {
|
|
17666
|
+
seq: inSeq,
|
|
17667
|
+
lastSeen: authenticated.lastSeen,
|
|
17668
|
+
viewerId: authenticated.viewerKey
|
|
17669
|
+
});
|
|
17670
|
+
} else if (authenticated.reason === "invalid_viewer") {
|
|
17671
|
+
log25.warn("rejecting input with invalid viewerId");
|
|
17672
|
+
} else {
|
|
15023
17673
|
log25.warn("failed to decrypt input \u2014 ignoring");
|
|
15024
17674
|
}
|
|
15025
17675
|
}
|
|
@@ -15043,39 +17693,53 @@ async function runAgent(rawOptions) {
|
|
|
15043
17693
|
} else if (payload.type === "control" && typeof payload.action === "string") {
|
|
15044
17694
|
let controlPayload = payload;
|
|
15045
17695
|
if (typeof payload.enc === "string" && typeof payload.iv === "string" && conn.sessionKey) {
|
|
17696
|
+
if (payload.action !== "upload-file") {
|
|
17697
|
+
log25.warn("rejecting encrypted control with unsupported outer action");
|
|
17698
|
+
return;
|
|
17699
|
+
}
|
|
15046
17700
|
const inSeq = typeof payload.seq === "number" ? payload.seq : null;
|
|
15047
|
-
const rxSeq = conn.getRxSeqForViewer(payload.viewerId);
|
|
15048
17701
|
if (inSeq === null) {
|
|
15049
17702
|
log25.warn("rejecting encrypted control frame without seq");
|
|
15050
17703
|
return;
|
|
15051
17704
|
}
|
|
15052
|
-
|
|
17705
|
+
const authenticated = conn.authenticateInboundFrame(
|
|
17706
|
+
payload.enc,
|
|
17707
|
+
payload.iv,
|
|
17708
|
+
inSeq,
|
|
17709
|
+
payload.viewerId,
|
|
17710
|
+
{
|
|
17711
|
+
bindingVersion: payload.bindingVersion,
|
|
17712
|
+
sentAtMs: payload.sentAtMs,
|
|
17713
|
+
type: "control",
|
|
17714
|
+
action: payload.action,
|
|
17715
|
+
sessionId: payload.sessionId
|
|
17716
|
+
}
|
|
17717
|
+
);
|
|
17718
|
+
if (!authenticated.ok && authenticated.reason === "replayed") {
|
|
15053
17719
|
log25.warn("rejecting replayed/out-of-order control", {
|
|
15054
17720
|
seq: inSeq,
|
|
15055
|
-
lastSeen:
|
|
15056
|
-
viewerId:
|
|
17721
|
+
lastSeen: authenticated.lastSeen,
|
|
17722
|
+
viewerId: authenticated.viewerKey
|
|
15057
17723
|
});
|
|
15058
17724
|
return;
|
|
15059
17725
|
}
|
|
15060
|
-
|
|
15061
|
-
payload.enc,
|
|
15062
|
-
payload.iv,
|
|
15063
|
-
conn.sessionKey,
|
|
15064
|
-
inSeq
|
|
15065
|
-
);
|
|
15066
|
-
if (decrypted === null) {
|
|
17726
|
+
if (!authenticated.ok) {
|
|
15067
17727
|
log25.warn("failed to decrypt control \u2014 ignoring");
|
|
15068
17728
|
return;
|
|
15069
17729
|
}
|
|
15070
|
-
|
|
15071
|
-
|
|
15072
|
-
|
|
15073
|
-
|
|
15074
|
-
|
|
15075
|
-
|
|
15076
|
-
|
|
17730
|
+
const verifiedUpload = parseAuthenticatedUploadControlPayload(
|
|
17731
|
+
authenticated.plaintext,
|
|
17732
|
+
{
|
|
17733
|
+
action: payload.action,
|
|
17734
|
+
sessionId: payload.sessionId,
|
|
17735
|
+
viewerId: payload.viewerId
|
|
17736
|
+
}
|
|
17737
|
+
);
|
|
17738
|
+
if (!verifiedUpload) {
|
|
17739
|
+
log25.warn("rejecting encrypted control with mismatched or invalid inner envelope");
|
|
15077
17740
|
return;
|
|
15078
17741
|
}
|
|
17742
|
+
controlPayload = verifiedUpload;
|
|
15079
17743
|
} else if (conn.sessionKey && payload.action === "upload-file") {
|
|
15080
17744
|
log25.warn("rejecting plaintext attachment upload while encrypted channel is active");
|
|
15081
17745
|
return;
|
|
@@ -15155,6 +17819,7 @@ async function runAgent(rawOptions) {
|
|
|
15155
17819
|
}
|
|
15156
17820
|
} finally {
|
|
15157
17821
|
dispatcher.stopTranscriptWatchers();
|
|
17822
|
+
nativeTranscriptRecovery?.cancel();
|
|
15158
17823
|
stopAllNativeSessions();
|
|
15159
17824
|
conn.cleanup({ stopStreams: true });
|
|
15160
17825
|
shell.stop();
|