@vikrant82/opencode-cache-keepalive 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +57 -0
- package/README.md +75 -48
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1097 -614
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts +8 -25
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/control.d.ts +10 -4
- package/dist/lib/control.d.ts.map +1 -1
- package/dist/lib/paths.d.ts +5 -12
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/lib/replay-warmer.d.ts +95 -0
- package/dist/lib/replay-warmer.d.ts.map +1 -0
- package/dist/lib/state.d.ts +15 -114
- package/dist/lib/state.d.ts.map +1 -1
- package/dist/lib/tui/commands.d.ts +2 -0
- package/dist/lib/tui/commands.d.ts.map +1 -1
- package/dist/lib/tui/footer.d.ts +5 -5
- package/dist/lib/tui/footer.d.ts.map +1 -1
- package/dist/lib/tui/format.d.ts +8 -0
- package/dist/lib/tui/format.d.ts.map +1 -1
- package/dist/lib/types.d.ts +123 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/tui.d.ts +3 -0
- package/dist/tui.d.ts.map +1 -1
- package/lib/config.ts +106 -74
- package/lib/control.ts +69 -20
- package/lib/paths.ts +12 -14
- package/lib/replay-warmer.ts +1200 -0
- package/lib/state.ts +172 -175
- package/lib/tui/commands.tsx +76 -0
- package/lib/tui/footer.tsx +55 -130
- package/lib/tui/format.ts +40 -0
- package/lib/types.ts +130 -0
- package/package.json +3 -2
- package/tui.tsx +40 -0
- package/dist/lib/keepalive.d.ts +0 -85
- package/dist/lib/keepalive.d.ts.map +0 -1
- package/dist/lib/model.d.ts +0 -8
- package/dist/lib/model.d.ts.map +0 -1
- package/dist/lib/system.d.ts +0 -11
- package/dist/lib/system.d.ts.map +0 -1
- package/lib/keepalive.ts +0 -636
- package/lib/model.ts +0 -19
- package/lib/system.ts +0 -19
- package/lib/tui/commands.ts +0 -59
package/dist/index.js
CHANGED
|
@@ -25,69 +25,103 @@ function configureClientAuth(client) {
|
|
|
25
25
|
// lib/config.ts
|
|
26
26
|
var DEFAULTS = {
|
|
27
27
|
enabled: true,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
pingToken: "~",
|
|
34
|
-
revertPing: true,
|
|
28
|
+
intervals: { claude: 285e3, gpt: 168e4 },
|
|
29
|
+
hosts: ["githubcopilot.com"],
|
|
30
|
+
cacheReadFactor: 0.1,
|
|
31
|
+
missFactor: 1,
|
|
32
|
+
maxReplaysPerGap: "auto",
|
|
35
33
|
includeChildSessions: false,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
replayTimeoutMs: 6e4,
|
|
35
|
+
maxStoredBytes: 67108864,
|
|
36
|
+
debug: false
|
|
39
37
|
};
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
var DEPRECATED = [
|
|
39
|
+
"intervalMs",
|
|
40
|
+
"intervalSeconds",
|
|
41
|
+
"windowMs",
|
|
42
|
+
"windowMinutes",
|
|
43
|
+
"revertPing",
|
|
44
|
+
"pingToken",
|
|
45
|
+
"injectSystemInstruction",
|
|
46
|
+
"providerAllowlist",
|
|
47
|
+
"modelAllowlist",
|
|
48
|
+
"claudeBusyWarm",
|
|
49
|
+
"claudeBusyWarmIntervalMs",
|
|
50
|
+
"claudeBusyWarmWindowMs"
|
|
51
|
+
];
|
|
52
|
+
var warnedDeprecated = /* @__PURE__ */ new Set();
|
|
53
|
+
function getConfig(options, warn = () => {
|
|
54
|
+
}) {
|
|
55
|
+
const option = options ?? {};
|
|
42
56
|
const env = process.env;
|
|
57
|
+
for (const key of DEPRECATED) {
|
|
58
|
+
if (key in option && !warnedDeprecated.has(key)) {
|
|
59
|
+
warnedDeprecated.add(key);
|
|
60
|
+
warn(`deprecated option ${key} is ignored`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const parseJson = (raw) => {
|
|
64
|
+
if (!raw) return void 0;
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(raw);
|
|
67
|
+
} catch {
|
|
68
|
+
return void 0;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const intervals = objectOption(
|
|
72
|
+
option.intervals ?? parseJson(env.OPENCODE_KEEPALIVE_INTERVALS),
|
|
73
|
+
DEFAULTS.intervals
|
|
74
|
+
);
|
|
75
|
+
const hosts = listOption(
|
|
76
|
+
option.hosts ?? parseJson(env.OPENCODE_KEEPALIVE_HOSTS),
|
|
77
|
+
DEFAULTS.hosts
|
|
78
|
+
);
|
|
79
|
+
const maxRaw = option.maxReplaysPerGap ?? env.OPENCODE_KEEPALIVE_MAX_REPLAYS_PER_GAP;
|
|
80
|
+
const maxReplaysPerGap = maxRaw === "auto" ? "auto" : positiveNumber(maxRaw) !== void 0 ? positiveNumber(maxRaw) : DEFAULTS.maxReplaysPerGap;
|
|
43
81
|
return {
|
|
44
|
-
enabled:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
o.intervalSeconds,
|
|
48
|
-
numEnv(env.OPENCODE_KEEPALIVE_INTERVAL_MS, DEFAULTS.intervalMs)
|
|
49
|
-
),
|
|
50
|
-
windowMs: durationOpt(
|
|
51
|
-
o.windowMs,
|
|
52
|
-
o.windowMinutes ? Number(o.windowMinutes) * 60 : void 0,
|
|
53
|
-
numEnv(env.OPENCODE_KEEPALIVE_WINDOW_MS, DEFAULTS.windowMs)
|
|
82
|
+
enabled: boolOption(
|
|
83
|
+
option.enabled,
|
|
84
|
+
env.OPENCODE_KEEPALIVE_ENABLED === void 0 ? DEFAULTS.enabled : env.OPENCODE_KEEPALIVE_ENABLED !== "false"
|
|
54
85
|
),
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
86
|
+
intervals,
|
|
87
|
+
hosts,
|
|
88
|
+
cacheReadFactor: positiveNumber(option.cacheReadFactor ?? env.OPENCODE_KEEPALIVE_CACHE_READ_FACTOR) ?? DEFAULTS.cacheReadFactor,
|
|
89
|
+
missFactor: positiveNumber(option.missFactor ?? env.OPENCODE_KEEPALIVE_MISS_FACTOR) ?? DEFAULTS.missFactor,
|
|
90
|
+
maxReplaysPerGap,
|
|
91
|
+
includeChildSessions: boolOption(
|
|
92
|
+
option.includeChildSessions,
|
|
93
|
+
env.OPENCODE_KEEPALIVE_INCLUDE_CHILD_SESSIONS === "true" || DEFAULTS.includeChildSessions
|
|
58
94
|
),
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
env.OPENCODE_KEEPALIVE_REVERT_PING !== "false" && DEFAULTS.revertPing
|
|
63
|
-
),
|
|
64
|
-
includeChildSessions: boolOpt(o.includeChildSessions, DEFAULTS.includeChildSessions),
|
|
65
|
-
debug: boolOpt(o.debug, env.OPENCODE_KEEPALIVE_DEBUG === "true"),
|
|
66
|
-
providerAllowlist: listOpt(o.providerAllowlist, DEFAULTS.providerAllowlist),
|
|
67
|
-
modelAllowlist: listOpt(o.modelAllowlist, DEFAULTS.modelAllowlist)
|
|
95
|
+
replayTimeoutMs: positiveNumber(option.replayTimeoutMs ?? env.OPENCODE_KEEPALIVE_REPLAY_TIMEOUT_MS) ?? DEFAULTS.replayTimeoutMs,
|
|
96
|
+
maxStoredBytes: positiveNumber(option.maxStoredBytes ?? env.OPENCODE_KEEPALIVE_MAX_STORED_BYTES) ?? DEFAULTS.maxStoredBytes,
|
|
97
|
+
debug: boolOption(option.debug, env.OPENCODE_KEEPALIVE_DEBUG === "true")
|
|
68
98
|
};
|
|
69
99
|
}
|
|
70
|
-
function
|
|
100
|
+
function boolOption(value, fallback) {
|
|
71
101
|
return typeof value === "boolean" ? value : fallback;
|
|
72
102
|
}
|
|
73
|
-
function
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return filtered.length > 0 ? filtered : fallback;
|
|
103
|
+
function positiveNumber(value) {
|
|
104
|
+
const parsed = typeof value === "string" ? Number(value) : value;
|
|
105
|
+
return typeof parsed === "number" && Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
77
106
|
}
|
|
78
|
-
function
|
|
79
|
-
if (typeof
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
107
|
+
function objectOption(value, fallback) {
|
|
108
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return fallback;
|
|
109
|
+
const entries = Object.entries(value).flatMap(([key, interval]) => {
|
|
110
|
+
const parsed = positiveNumber(interval);
|
|
111
|
+
return parsed === void 0 ? [] : [[key.toLowerCase(), parsed]];
|
|
112
|
+
});
|
|
113
|
+
return entries.length ? Object.fromEntries(entries) : fallback;
|
|
83
114
|
}
|
|
84
|
-
function
|
|
85
|
-
if (!
|
|
86
|
-
const
|
|
87
|
-
|
|
115
|
+
function listOption(value, fallback) {
|
|
116
|
+
if (!Array.isArray(value)) return fallback;
|
|
117
|
+
const values = value.filter(
|
|
118
|
+
(item) => typeof item === "string" && item.length > 0
|
|
119
|
+
);
|
|
120
|
+
return values.length ? values : fallback;
|
|
88
121
|
}
|
|
89
122
|
|
|
90
123
|
// lib/control.ts
|
|
124
|
+
import { randomUUID } from "crypto";
|
|
91
125
|
import { readFileSync } from "fs";
|
|
92
126
|
import { mkdir, rename, writeFile } from "fs/promises";
|
|
93
127
|
import { dirname } from "path";
|
|
@@ -96,9 +130,14 @@ import { dirname } from "path";
|
|
|
96
130
|
import { createHash } from "crypto";
|
|
97
131
|
import { homedir } from "os";
|
|
98
132
|
import { join } from "path";
|
|
99
|
-
function
|
|
100
|
-
|
|
101
|
-
|
|
133
|
+
function stateDirectoryKey(directory) {
|
|
134
|
+
return createHash("sha256").update(directory).digest("hex").slice(0, 16);
|
|
135
|
+
}
|
|
136
|
+
function instanceStateFilePath(directory, pid, instanceId) {
|
|
137
|
+
return join(
|
|
138
|
+
stateDirectoryPath(),
|
|
139
|
+
`state-v2-${stateDirectoryKey(directory)}-${pid}-${instanceId}.json`
|
|
140
|
+
);
|
|
102
141
|
}
|
|
103
142
|
function stateDirectoryPath() {
|
|
104
143
|
return join(
|
|
@@ -118,526 +157,971 @@ function logFilePath() {
|
|
|
118
157
|
}
|
|
119
158
|
|
|
120
159
|
// lib/control.ts
|
|
121
|
-
|
|
160
|
+
var VERSION = 2;
|
|
161
|
+
var MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
162
|
+
function readSessionControls(directory, now = Date.now()) {
|
|
122
163
|
try {
|
|
123
|
-
const value = JSON.parse(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
164
|
+
const value = JSON.parse(readFileSync(controlFilePath(directory), "utf8"));
|
|
165
|
+
if (value.version !== VERSION || !value.sessions || typeof value.sessions !== "object")
|
|
166
|
+
return { version: VERSION, sessions: {} };
|
|
167
|
+
const sessions = {};
|
|
168
|
+
for (const [id, raw] of Object.entries(value.sessions)) {
|
|
169
|
+
if (!raw || typeof raw !== "object" || typeof raw.enabled !== "boolean" || typeof raw.updatedAt !== "number" || raw.updatedAt < now - MAX_AGE_MS)
|
|
170
|
+
continue;
|
|
171
|
+
sessions[id] = {
|
|
172
|
+
enabled: raw.enabled,
|
|
173
|
+
updatedAt: raw.updatedAt
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return { version: VERSION, sessions };
|
|
129
177
|
} catch {
|
|
130
|
-
return
|
|
178
|
+
return { version: VERSION, sessions: {} };
|
|
131
179
|
}
|
|
132
180
|
}
|
|
181
|
+
var updateQueue = Promise.resolve();
|
|
133
182
|
|
|
134
|
-
// lib/
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const providerOk = config.providerAllowlist.some((needle) => provider.includes(needle));
|
|
140
|
-
const modelOk = config.modelAllowlist.some((needle) => model.includes(needle));
|
|
141
|
-
return providerOk && modelOk;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// lib/keepalive.ts
|
|
145
|
-
var TICK_MS = 15e3;
|
|
146
|
-
var JITTER_MS = 15e3;
|
|
147
|
-
var CONTROL_POLL_MS = 1e3;
|
|
148
|
-
var STATUS_POLL_MS = 2e3;
|
|
149
|
-
var PING_STATUS_GRACE_MS = 5e3;
|
|
150
|
-
var COLD_AFTER_INTERVALS = 2;
|
|
151
|
-
var MESSAGE_LOOKUP_LIMIT = 50;
|
|
152
|
-
var KeepaliveEngine = class {
|
|
153
|
-
constructor(client, config, store, logger, directory) {
|
|
183
|
+
// lib/replay-warmer.ts
|
|
184
|
+
var STATE_KEY = /* @__PURE__ */ Symbol.for("opencode-cache-keepalive.replay-warmer");
|
|
185
|
+
var RETRY_MS = 1e4;
|
|
186
|
+
var ReplayWarmer = class {
|
|
187
|
+
constructor(client, config, sink, logger, directory, now = Date.now) {
|
|
154
188
|
this.client = client;
|
|
155
189
|
this.config = config;
|
|
156
|
-
this.
|
|
190
|
+
this.sink = sink;
|
|
157
191
|
this.logger = logger;
|
|
158
192
|
this.directory = directory;
|
|
159
|
-
|
|
160
|
-
this.enabled = control?.enabled ?? config.enabled;
|
|
161
|
-
this.controlUpdatedAt = control?.updatedAt ?? 0;
|
|
162
|
-
this.store.setEnabled(this.enabled);
|
|
193
|
+
this.now = now;
|
|
163
194
|
}
|
|
164
195
|
client;
|
|
165
196
|
config;
|
|
166
|
-
|
|
197
|
+
sink;
|
|
167
198
|
logger;
|
|
168
199
|
directory;
|
|
169
|
-
|
|
200
|
+
now;
|
|
201
|
+
runtime;
|
|
170
202
|
controlTimer;
|
|
171
|
-
|
|
172
|
-
controlUpdatedAt = 0;
|
|
173
|
-
reconcilingStatus = false;
|
|
174
|
-
enabled;
|
|
203
|
+
enabled = false;
|
|
175
204
|
start() {
|
|
176
|
-
if (this.
|
|
177
|
-
|
|
178
|
-
this.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
205
|
+
if (this.runtime) return;
|
|
206
|
+
const runtime = getRuntime(this.now);
|
|
207
|
+
this.enabled = this.config.enabled;
|
|
208
|
+
runtime.instances.set(this, {
|
|
209
|
+
enabled: this.enabled,
|
|
210
|
+
effective: /* @__PURE__ */ new Map(),
|
|
211
|
+
directory: this.directory,
|
|
212
|
+
controls: new Map(
|
|
213
|
+
Object.entries(readSessionControls(this.directory, this.now()).sessions).map(
|
|
214
|
+
([id, value]) => [id, value.enabled]
|
|
215
|
+
)
|
|
216
|
+
),
|
|
217
|
+
hosts: new Set(this.config.hosts.map((host) => host.toLowerCase())),
|
|
218
|
+
config: this.config,
|
|
219
|
+
sink: this.sink
|
|
220
|
+
});
|
|
221
|
+
runtime.maxStoredBytes = Math.min(runtime.maxStoredBytes, this.config.maxStoredBytes);
|
|
222
|
+
for (const host of this.config.hosts) runtime.hosts.add(host.toLowerCase());
|
|
223
|
+
runtime.instancesSet.add(this);
|
|
224
|
+
this.runtime = runtime;
|
|
225
|
+
if (!runtime.pruneTimer) {
|
|
226
|
+
runtime.pruneTimer = setInterval(() => pruneOrphanSessions(runtime), 6e4);
|
|
227
|
+
}
|
|
228
|
+
if (runtime.instancesSet.size === 1) {
|
|
229
|
+
globalThis.fetch = ((input, init) => {
|
|
230
|
+
let bodySource;
|
|
231
|
+
let rejection;
|
|
232
|
+
let shouldRecord = false;
|
|
233
|
+
try {
|
|
234
|
+
rejection = cheapEligibilityRejection(runtime, input, init);
|
|
235
|
+
if (rejection) {
|
|
236
|
+
if (isCopilotRequest(input))
|
|
237
|
+
runtime.instancesSet.values().next().value?.debugRecordRejected(rejection);
|
|
238
|
+
} else if (!rejection) {
|
|
239
|
+
shouldRecord = true;
|
|
240
|
+
if (isReplayRequest(input, init) && input instanceof Request && init?.body === void 0) {
|
|
241
|
+
bodySource = input.clone().text();
|
|
242
|
+
void bodySource.catch(() => {
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} catch {
|
|
247
|
+
}
|
|
248
|
+
const result = runtime.originalFetch(input, init);
|
|
249
|
+
if (shouldRecord) {
|
|
250
|
+
queueMicrotask(() => {
|
|
251
|
+
void recordRequest(runtime, input, init, bodySource).catch(() => {
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return result;
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
this.controlTimer = setInterval(() => this.syncControl(), 1e3);
|
|
184
259
|
}
|
|
185
|
-
|
|
186
|
-
if (!this.timer) return;
|
|
187
|
-
clearInterval(this.timer);
|
|
188
|
-
this.timer = void 0;
|
|
260
|
+
dispose() {
|
|
189
261
|
if (this.controlTimer) clearInterval(this.controlTimer);
|
|
190
262
|
this.controlTimer = void 0;
|
|
191
|
-
|
|
192
|
-
this.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
263
|
+
const runtime = this.runtime;
|
|
264
|
+
this.runtime = void 0;
|
|
265
|
+
if (!runtime) return;
|
|
266
|
+
runtime.instancesSet.delete(this);
|
|
267
|
+
runtime.instances.delete(this);
|
|
268
|
+
for (const [id, owner] of runtime.owners) {
|
|
269
|
+
if (owner !== this) continue;
|
|
270
|
+
const session = runtime.sessions.get(id);
|
|
271
|
+
if (session) {
|
|
272
|
+
this.clearTimerFrom(session);
|
|
273
|
+
session.controller?.abort();
|
|
274
|
+
runtime.storedBytes = Math.max(
|
|
275
|
+
0,
|
|
276
|
+
runtime.storedBytes - bodyBytes(session.request.body)
|
|
277
|
+
);
|
|
278
|
+
session.request.body = "";
|
|
279
|
+
}
|
|
280
|
+
runtime.owners.delete(id);
|
|
281
|
+
runtime.sessions.delete(id);
|
|
282
|
+
}
|
|
283
|
+
if (runtime.instancesSet.size === 0) {
|
|
284
|
+
if (runtime.pruneTimer) clearInterval(runtime.pruneTimer);
|
|
285
|
+
globalThis.fetch = runtime.originalFetch;
|
|
286
|
+
for (const [id, session] of runtime.sessions) {
|
|
287
|
+
this.clearTimerFrom(session);
|
|
288
|
+
session.controller?.abort();
|
|
289
|
+
runtime.sessions.delete(id);
|
|
290
|
+
}
|
|
291
|
+
delete globalThis[STATE_KEY];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
onEvent(event) {
|
|
295
|
+
const runtime = this.runtime;
|
|
296
|
+
if (!runtime) return;
|
|
297
|
+
const props = event?.properties ?? {};
|
|
298
|
+
const id = props.sessionID ?? props.info?.id ?? props.part?.sessionID;
|
|
299
|
+
const info = props.info ?? props.session;
|
|
300
|
+
if (event?.type === "session.created" && typeof info?.id === "string" && info.parentID)
|
|
301
|
+
runtime.childSessions.add(info.id);
|
|
302
|
+
if (event?.type === "session.deleted" && typeof id === "string")
|
|
303
|
+
runtime.childSessions.delete(id);
|
|
304
|
+
if (typeof id !== "string") return;
|
|
305
|
+
if (event?.type !== "session.deleted" && !this.sessionEnabled(id)) {
|
|
306
|
+
const disabled = runtime.sessions.get(id);
|
|
307
|
+
if (disabled && runtime.owners.get(id) === this) {
|
|
308
|
+
this.disableSession(runtime, id, disabled);
|
|
309
|
+
} else if (!disabled && this.config.enabled && this.runtime?.instances.get(this)?.controls.get(id) === false) {
|
|
310
|
+
runtime.owners.set(id, this);
|
|
311
|
+
this.sink.upsert({
|
|
312
|
+
sessionID: id,
|
|
313
|
+
updatedAt: this.now(),
|
|
314
|
+
enabled: false,
|
|
315
|
+
model: "",
|
|
316
|
+
api: "messages",
|
|
317
|
+
state: "off",
|
|
318
|
+
gapId: 0,
|
|
319
|
+
gapReplays: 0,
|
|
320
|
+
cap: this.cap,
|
|
321
|
+
nextReplayAt: null,
|
|
322
|
+
sessionReplays: 0,
|
|
323
|
+
avoidedTokens: 0,
|
|
324
|
+
resumeHits: 0,
|
|
325
|
+
resumeCount: 0
|
|
326
|
+
});
|
|
211
327
|
}
|
|
212
328
|
return;
|
|
213
329
|
}
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (session?.warming) this.notePingRunPart(session, part);
|
|
330
|
+
if (!runtime.owners.has(id)) runtime.owners.set(id, this);
|
|
331
|
+
if (runtime.owners.get(id) !== this) return;
|
|
332
|
+
const session = runtime.sessions.get(id);
|
|
333
|
+
if (!session && event.type === "session.deleted") {
|
|
334
|
+
this.sink.remove(id);
|
|
220
335
|
return;
|
|
221
336
|
}
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
337
|
+
if (!session) return;
|
|
338
|
+
session.touchedAt = this.now();
|
|
339
|
+
const type = event.type;
|
|
340
|
+
if (type === "session.deleted") {
|
|
341
|
+
this.clearTimer(session);
|
|
342
|
+
session.controller?.abort();
|
|
343
|
+
runtime.sessions.delete(id);
|
|
344
|
+
runtime.storedBytes = Math.max(0, runtime.storedBytes - bodyBytes(session.request.body));
|
|
345
|
+
session.request.body = "";
|
|
346
|
+
this.sink.remove(id);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (type === "session.status" || type === "session.idle") {
|
|
350
|
+
const status = props.status?.type ?? "idle";
|
|
351
|
+
if (status === "idle") {
|
|
352
|
+
session.active = false;
|
|
353
|
+
session.busy = false;
|
|
354
|
+
session.runningTools.clear();
|
|
355
|
+
this.updateState(session, "idle");
|
|
356
|
+
this.schedule(id, session, session.request.sentAt + session.request.intervalMs);
|
|
357
|
+
} else if (status === "busy" || status === "running") {
|
|
358
|
+
session.active = session.runningTools.size === 0;
|
|
359
|
+
session.busy = session.runningTools.size > 0;
|
|
360
|
+
if (session.active) this.clearTimer(session);
|
|
361
|
+
this.updateState(session, session.active ? "active" : "busy");
|
|
230
362
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
s.active = false;
|
|
240
|
-
this.store.persist();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (type === "message.updated") {
|
|
366
|
+
const info2 = props.info ?? {};
|
|
367
|
+
const interactionType = String(info2.interactionType ?? props.interactionType ?? "");
|
|
368
|
+
if (/agent-session-(name|summary)-generation/i.test(interactionType)) return;
|
|
369
|
+
if (info2.role === "assistant" && (typeof info2.providerID === "string" && !info2.providerID.toLowerCase().replace(/[^a-z]/g, "").includes("copilot") || typeof info2.modelID === "string" && !info2.modelID.toLowerCase().includes(session.request.model.toLowerCase()) && !session.request.model.toLowerCase().includes(info2.modelID.toLowerCase()))) {
|
|
370
|
+
this.invalidateTarget(runtime, session, this.now());
|
|
241
371
|
return;
|
|
242
372
|
}
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
this.
|
|
373
|
+
if (info2.role === "assistant" && info2.time?.completed === void 0) {
|
|
374
|
+
session.active = true;
|
|
375
|
+
this.clearTimer(session);
|
|
376
|
+
this.updateState(session, "active");
|
|
247
377
|
}
|
|
248
378
|
return;
|
|
249
379
|
}
|
|
250
|
-
if (type
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
380
|
+
if (type !== "message.part.updated") return;
|
|
381
|
+
const part = props.part;
|
|
382
|
+
if (part?.type === "step-start") {
|
|
383
|
+
session.active = true;
|
|
384
|
+
this.clearTimer(session);
|
|
385
|
+
this.updateState(session, "active");
|
|
386
|
+
} else if (part?.type === "step-finish") {
|
|
387
|
+
session.active = false;
|
|
388
|
+
const cache = part.tokens?.cache ?? {};
|
|
389
|
+
const read = Number(cache.read ?? 0);
|
|
390
|
+
const prompt = Number(part.tokens?.input ?? 0) + read + Number(cache.write ?? 0);
|
|
391
|
+
const completedGap = session.pendingResume;
|
|
392
|
+
if (completedGap) {
|
|
393
|
+
const hit = read >= 0.9 * completedGap.promptPrev;
|
|
394
|
+
const avoidedTokens = hit ? completedGap.promptPrev : 0;
|
|
395
|
+
const resume = {
|
|
396
|
+
at: this.now(),
|
|
397
|
+
read,
|
|
398
|
+
promptPrev: completedGap.promptPrev,
|
|
399
|
+
hit,
|
|
400
|
+
replays: completedGap.replays,
|
|
401
|
+
avoidedTokens,
|
|
402
|
+
replayReadTokens: completedGap.replayReadTokens
|
|
403
|
+
};
|
|
404
|
+
session.entry.resume = resume;
|
|
405
|
+
session.entry.avoidedTokens += avoidedTokens;
|
|
406
|
+
session.entry.resumeCount++;
|
|
407
|
+
if (hit) session.entry.resumeHits++;
|
|
408
|
+
const totals = this.totals;
|
|
409
|
+
totals.avoidedTokens += avoidedTokens;
|
|
410
|
+
totals.replayReadTokens += completedGap.replayReadTokens;
|
|
411
|
+
if (hit) totals.resumeHits++;
|
|
412
|
+
else totals.resumeMisses++;
|
|
413
|
+
this.sink.setTotals(totals);
|
|
414
|
+
this.logger.info(
|
|
415
|
+
`resume session=${id} gap=${session.entry.gapId} hit=${hit} read=${read} prev=${completedGap.promptPrev} replays=${completedGap.replays} avoided=${avoidedTokens} replayRead=${completedGap.replayReadTokens}`
|
|
416
|
+
);
|
|
417
|
+
session.pendingResume = void 0;
|
|
418
|
+
}
|
|
419
|
+
session.previousPrompt = prompt;
|
|
420
|
+
if (session.busy) this.updateState(session, "busy");
|
|
421
|
+
else {
|
|
422
|
+
this.updateState(session, "idle");
|
|
423
|
+
this.schedule(id, session, session.request.sentAt + session.request.intervalMs);
|
|
424
|
+
}
|
|
425
|
+
} else if (part?.type === "tool" && typeof part.callID === "string") {
|
|
426
|
+
if (part.state?.status === "running") session.runningTools.add(part.callID);
|
|
427
|
+
else if (["completed", "error"].includes(part.state?.status))
|
|
428
|
+
session.runningTools.delete(part.callID);
|
|
429
|
+
session.busy = session.runningTools.size > 0;
|
|
430
|
+
if (session.busy) {
|
|
431
|
+
session.active = false;
|
|
432
|
+
this.updateState(session, "busy");
|
|
433
|
+
this.schedule(id, session, session.request.sentAt + session.request.intervalMs);
|
|
434
|
+
} else if (!session.active) {
|
|
435
|
+
this.updateState(session, "idle");
|
|
436
|
+
this.schedule(id, session, session.request.sentAt + session.request.intervalMs);
|
|
260
437
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
totals = {
|
|
441
|
+
replays: 0,
|
|
442
|
+
avoidedTokens: 0,
|
|
443
|
+
replayReadTokens: 0,
|
|
444
|
+
resumeHits: 0,
|
|
445
|
+
resumeMisses: 0
|
|
446
|
+
};
|
|
447
|
+
/** @internal Called asynchronously by the process-wide recorder. */
|
|
448
|
+
debugRecordRejected(reason) {
|
|
449
|
+
this.logger.dbg(`record rejected reason=${reason}`);
|
|
450
|
+
}
|
|
451
|
+
/** @internal Called asynchronously by the process-wide recorder. */
|
|
452
|
+
async recordRequest(runtime, input, init, bodySource, selectedBody, selectedParsed) {
|
|
453
|
+
if (!this.runtime) return;
|
|
454
|
+
const request = input instanceof Request ? input : void 0;
|
|
455
|
+
const body = init?.body;
|
|
456
|
+
const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
|
|
457
|
+
const url = input instanceof URL ? input.href : typeof input === "string" ? input : input.url;
|
|
458
|
+
let u;
|
|
459
|
+
try {
|
|
460
|
+
u = new URL(url);
|
|
461
|
+
} catch {
|
|
462
|
+
this.debugRecordRejected("invalid-url");
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (!this.config.hosts.some((host) => {
|
|
466
|
+
const allowed = host.toLowerCase();
|
|
467
|
+
const hostname = u.hostname.toLowerCase();
|
|
468
|
+
return hostname === allowed || hostname.endsWith(`.${allowed}`);
|
|
469
|
+
})) {
|
|
470
|
+
if (isCopilotHost(u.hostname)) this.debugRecordRejected("host-not-allowed");
|
|
265
471
|
return;
|
|
266
472
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (this.
|
|
271
|
-
await this.armWindow(sessionID);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* Track a new user message observed while a ping is in flight. Event order alone
|
|
276
|
-
* cannot tell the ping's own message from a real prompt racing it, so messages are
|
|
277
|
-
* held until their text part arrives (see `notePingRunPart`). Any message seen
|
|
278
|
-
* before the ping is sent, or a second new message, is necessarily a real turn.
|
|
279
|
-
*/
|
|
280
|
-
notePingRunUser(s, info) {
|
|
281
|
-
const ping = s.ping;
|
|
282
|
-
if (!ping || typeof info?.id !== "string") return;
|
|
283
|
-
const created = info?.time?.created;
|
|
284
|
-
if (typeof created === "number" && created < ping.startedAt) return;
|
|
285
|
-
if (info.id === ping.messageID || ping.pending.has(info.id)) return;
|
|
286
|
-
if (!ping.sent || ping.messageID) {
|
|
287
|
-
this.interruptPing(s, info);
|
|
473
|
+
const headers = new Headers(request?.headers);
|
|
474
|
+
if (init?.headers) new Headers(init.headers).forEach((v, k) => headers.set(k, v));
|
|
475
|
+
if (/agent-session-(name|summary)-generation/i.test(headers.get("X-Interaction-Type") ?? "")) {
|
|
476
|
+
if (isCopilotHost(u.hostname)) this.debugRecordRejected("utility-interaction");
|
|
288
477
|
return;
|
|
289
478
|
}
|
|
290
|
-
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Classify a pending user message by its first text part: the ping token marks
|
|
295
|
-
* the ping's own message; any other text is a real turn joining the ping run.
|
|
296
|
-
* opencode publishes a user message's parts right after the message itself, before
|
|
297
|
-
* the run reaches any tool call.
|
|
298
|
-
*/
|
|
299
|
-
notePingRunPart(s, part) {
|
|
300
|
-
const ping = s.ping;
|
|
301
|
-
if (!ping || part?.type !== "text" || part?.synthetic) return;
|
|
302
|
-
const id = part?.messageID;
|
|
303
|
-
if (typeof id !== "string" || !ping.pending.has(id)) return;
|
|
304
|
-
const info = ping.pending.get(id);
|
|
305
|
-
ping.pending.delete(id);
|
|
306
|
-
if (!ping.messageID && String(part.text ?? "").trim() === this.config.pingToken) {
|
|
307
|
-
ping.messageID = id;
|
|
479
|
+
const sessionID = headers.get("X-Session-Id") ?? headers.get("x-session-affinity") ?? headers.get("X-Interaction-Id");
|
|
480
|
+
if (!sessionID) {
|
|
481
|
+
if (isCopilotHost(u.hostname)) this.debugRecordRejected("missing-session-id");
|
|
308
482
|
return;
|
|
309
483
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (!ping) return;
|
|
316
|
-
if (!ping.interrupted) this.logger.dbg(`real turn joined ping ${short(s.sessionID)}`);
|
|
317
|
-
ping.interrupted = true;
|
|
318
|
-
if (info) noteUserMessage(s, info);
|
|
319
|
-
s.busy = true;
|
|
320
|
-
s.active = false;
|
|
321
|
-
this.store.persist();
|
|
322
|
-
}
|
|
323
|
-
async armWindow(sessionID) {
|
|
324
|
-
const s = this.store.ensure(sessionID);
|
|
325
|
-
if (s.active && !s.busy) return;
|
|
326
|
-
if (s.arming) return;
|
|
327
|
-
s.arming = true;
|
|
484
|
+
const owner = runtime.owners.get(sessionID);
|
|
485
|
+
if (owner && owner !== this) return;
|
|
486
|
+
if (!runtime.instances.has(this) || !this.enabled || this.sessionEnabled(sessionID) === false)
|
|
487
|
+
return;
|
|
488
|
+
let stringBody;
|
|
328
489
|
try {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
490
|
+
stringBody = selectedBody !== void 0 ? selectedBody : typeof body === "string" ? body : body === void 0 && request && isReplayRequest(input, init) ? await (bodySource ?? request.clone().text()) : void 0;
|
|
491
|
+
} catch {
|
|
492
|
+
stringBody = void 0;
|
|
493
|
+
}
|
|
494
|
+
const instance = runtime.instances.get(this);
|
|
495
|
+
if (instance) {
|
|
496
|
+
instance.controls = new Map(
|
|
497
|
+
Object.entries(readSessionControls(this.directory, this.now()).sessions).map(
|
|
498
|
+
([id, value]) => [id, value.enabled]
|
|
499
|
+
)
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
if (!this.runtime || !instance || runtime.owners.has(sessionID) && runtime.owners.get(sessionID) !== this || !this.enabled || this.sessionEnabled(sessionID) === false)
|
|
503
|
+
return;
|
|
504
|
+
const session = runtime.sessions.get(sessionID);
|
|
505
|
+
const sentAt = this.now();
|
|
506
|
+
if (session) session.touchedAt = sentAt;
|
|
507
|
+
if (session) {
|
|
508
|
+
if (session.entry.gapReplays > 0 && session.previousPrompt !== void 0) {
|
|
509
|
+
session.pendingResume = {
|
|
510
|
+
promptPrev: session.previousPrompt,
|
|
511
|
+
replays: session.entry.gapReplays,
|
|
512
|
+
replayReadTokens: session.replayReadTokens
|
|
513
|
+
};
|
|
335
514
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
515
|
+
this.startGap(session, sentAt);
|
|
516
|
+
session.entry.gapId++;
|
|
517
|
+
}
|
|
518
|
+
if (method !== "POST" || !apiFor(u.pathname)) {
|
|
519
|
+
if (session) this.invalidateTarget(runtime, session, sentAt);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (stringBody === void 0) {
|
|
523
|
+
if (isCopilotHost(u.hostname)) this.debugRecordRejected("unsupported-body-type");
|
|
524
|
+
if (session) {
|
|
525
|
+
this.invalidateTarget(runtime, session, sentAt);
|
|
342
526
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
s.windowEndsAt = s.lastResponseAt + this.config.windowMs;
|
|
354
|
-
s.nextPingAt = Math.max(now, lastCacheTouch(s) + this.config.intervalMs);
|
|
355
|
-
s.pingsSent = 0;
|
|
356
|
-
s.lastPing = void 0;
|
|
357
|
-
s.active = true;
|
|
358
|
-
}
|
|
359
|
-
/** False once the cache has gone too long without a request to still be trusted warm. */
|
|
360
|
-
cacheMayBeWarm(s, now) {
|
|
361
|
-
const touch = lastCacheTouch(s);
|
|
362
|
-
return touch > 0 && now - touch <= COLD_AFTER_INTERVALS * this.config.intervalMs;
|
|
363
|
-
}
|
|
364
|
-
async tick() {
|
|
365
|
-
if (!this.enabled) return;
|
|
366
|
-
const now = Date.now();
|
|
367
|
-
for (const s of this.store.all()) {
|
|
368
|
-
if (!s.eligible || !s.active || s.busy || s.warming) continue;
|
|
369
|
-
if (now >= s.windowEndsAt) {
|
|
370
|
-
s.active = false;
|
|
371
|
-
this.store.persist();
|
|
372
|
-
this.logger.dbg(
|
|
373
|
-
`window closed ${short(s.sessionID)} after ${s.pingsSent} ping(s) \u2014 cache may cool`
|
|
374
|
-
);
|
|
375
|
-
continue;
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
let parsed;
|
|
530
|
+
try {
|
|
531
|
+
if (stringBody === void 0) return;
|
|
532
|
+
parsed = selectedBody !== void 0 ? selectedParsed : JSON.parse(stringBody);
|
|
533
|
+
} catch {
|
|
534
|
+
if (isCopilotHost(u.hostname)) this.debugRecordRejected("invalid-json-body");
|
|
535
|
+
if (session) {
|
|
536
|
+
this.invalidateTarget(runtime, session, sentAt);
|
|
376
537
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
this.recordEligible(runtime, sessionID, url, method, headers, stringBody, parsed, u);
|
|
541
|
+
}
|
|
542
|
+
recordEligible(runtime, sessionID, url, method, headers, stringBody, parsed, u) {
|
|
543
|
+
const child = runtime.childSessions.has(sessionID);
|
|
544
|
+
let session = runtime.sessions.get(sessionID);
|
|
545
|
+
const sentAt = this.now();
|
|
546
|
+
if (session) session.touchedAt = sentAt;
|
|
547
|
+
const eligible = this.sessionEnabled(sessionID) && typeof parsed?.model === "string" && (!child || this.config.includeChildSessions);
|
|
548
|
+
const model = typeof parsed?.model === "string" ? parsed.model.toLowerCase() : "";
|
|
549
|
+
const match = Object.entries(this.config.intervals).find(
|
|
550
|
+
([key]) => model.includes(key.toLowerCase())
|
|
551
|
+
);
|
|
552
|
+
const api = apiFor(u.pathname);
|
|
553
|
+
if (!eligible || !match || !api) {
|
|
554
|
+
if (isCopilotHost(u.hostname))
|
|
555
|
+
this.debugRecordRejected(
|
|
556
|
+
!this.sessionEnabled(sessionID) ? "session-disabled" : child && !this.config.includeChildSessions ? "child-session" : typeof parsed?.model !== "string" ? "missing-model" : !match ? "model-not-configured" : "unsupported-path"
|
|
383
557
|
);
|
|
384
|
-
|
|
558
|
+
if (session) {
|
|
559
|
+
this.invalidateTarget(runtime, session, sentAt);
|
|
385
560
|
}
|
|
386
|
-
|
|
561
|
+
return;
|
|
387
562
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
563
|
+
runtime.instances.get(this).effective.set(sessionID, true);
|
|
564
|
+
if (!runtime.loggedApis.has(api)) {
|
|
565
|
+
runtime.loggedApis.add(api);
|
|
566
|
+
this.logger.info(`record api=${api} host=${u.hostname}${u.pathname}`);
|
|
567
|
+
}
|
|
568
|
+
if (session) {
|
|
569
|
+
session.entry.gapReplays = 0;
|
|
570
|
+
session.entry.resume = void 0;
|
|
571
|
+
session.entry.lastReplay = void 0;
|
|
572
|
+
session.entry.stopReason = void 0;
|
|
573
|
+
session.entry.stopStatus = void 0;
|
|
574
|
+
session.entry.state = "active";
|
|
575
|
+
session.entry.enabled = true;
|
|
576
|
+
session.entry.model = parsed.model;
|
|
577
|
+
session.entry.api = api;
|
|
578
|
+
session.entry.nextReplayAt = null;
|
|
579
|
+
session.entry.updatedAt = sentAt;
|
|
580
|
+
session.active = true;
|
|
581
|
+
session.busy = false;
|
|
582
|
+
session.retries = 0;
|
|
583
|
+
}
|
|
584
|
+
runtime.owners.set(sessionID, this);
|
|
585
|
+
const oldSession = runtime.sessions.get(sessionID);
|
|
586
|
+
if (oldSession) {
|
|
587
|
+
runtime.storedBytes = Math.max(
|
|
588
|
+
0,
|
|
589
|
+
runtime.storedBytes - bodyBytes(oldSession.request.body)
|
|
590
|
+
);
|
|
591
|
+
oldSession.request = {
|
|
592
|
+
url,
|
|
593
|
+
method,
|
|
594
|
+
headers,
|
|
595
|
+
body: stringBody,
|
|
596
|
+
model: parsed.model,
|
|
597
|
+
api,
|
|
598
|
+
sentAt,
|
|
599
|
+
intervalMs: match[1]
|
|
600
|
+
};
|
|
601
|
+
runtime.storedBytes += bodyBytes(stringBody);
|
|
602
|
+
oldSession.child = child;
|
|
603
|
+
this.publish(oldSession);
|
|
604
|
+
this.enforceStoredBudget(runtime);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
session = {
|
|
608
|
+
request: {
|
|
609
|
+
url,
|
|
610
|
+
method,
|
|
611
|
+
headers,
|
|
612
|
+
body: stringBody,
|
|
613
|
+
model: parsed.model,
|
|
614
|
+
api,
|
|
615
|
+
sentAt,
|
|
616
|
+
intervalMs: match[1]
|
|
617
|
+
},
|
|
618
|
+
entry: {
|
|
619
|
+
sessionID,
|
|
620
|
+
updatedAt: sentAt,
|
|
621
|
+
enabled: true,
|
|
622
|
+
model: parsed.model,
|
|
623
|
+
api,
|
|
624
|
+
state: "active",
|
|
625
|
+
gapId: 1,
|
|
626
|
+
gapReplays: 0,
|
|
627
|
+
cap: this.cap,
|
|
628
|
+
nextReplayAt: null,
|
|
629
|
+
sessionReplays: 0,
|
|
630
|
+
avoidedTokens: 0,
|
|
631
|
+
resumeHits: 0,
|
|
632
|
+
resumeCount: 0
|
|
633
|
+
},
|
|
634
|
+
runningTools: /* @__PURE__ */ new Set(),
|
|
635
|
+
active: true,
|
|
636
|
+
busy: false,
|
|
637
|
+
realRequests: 1,
|
|
638
|
+
retries: 0,
|
|
639
|
+
child,
|
|
640
|
+
replayReadTokens: 0,
|
|
641
|
+
touchedAt: sentAt
|
|
397
642
|
};
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
643
|
+
runtime.sessions.set(sessionID, session);
|
|
644
|
+
runtime.owners.set(sessionID, this);
|
|
645
|
+
runtime.storedBytes += bodyBytes(stringBody);
|
|
646
|
+
this.publish(session);
|
|
647
|
+
this.enforceStoredBudget(runtime);
|
|
648
|
+
}
|
|
649
|
+
startGap(session, sentAt, model) {
|
|
650
|
+
const runtime = this.runtime;
|
|
651
|
+
session.controller?.abort();
|
|
652
|
+
session.controller = void 0;
|
|
653
|
+
this.clearTimer(session);
|
|
654
|
+
runtime.storedBytes = Math.max(0, runtime.storedBytes - bodyBytes(session.request.body));
|
|
655
|
+
session.request.body = "";
|
|
656
|
+
session.entry.gapReplays = 0;
|
|
657
|
+
session.entry.resume = void 0;
|
|
658
|
+
session.entry.lastReplay = void 0;
|
|
659
|
+
session.entry.stopReason = void 0;
|
|
660
|
+
session.entry.stopStatus = void 0;
|
|
661
|
+
session.entry.nextReplayAt = null;
|
|
662
|
+
session.replayReadTokens = 0;
|
|
663
|
+
session.entry.updatedAt = sentAt;
|
|
664
|
+
session.touchedAt = sentAt;
|
|
665
|
+
session.entry.model = model ?? session.entry.model;
|
|
666
|
+
session.active = false;
|
|
667
|
+
session.busy = false;
|
|
668
|
+
session.retries = 0;
|
|
669
|
+
}
|
|
670
|
+
invalidateTarget(runtime, session, at) {
|
|
671
|
+
this.clearTimer(session);
|
|
672
|
+
session.controller?.abort();
|
|
673
|
+
session.controller = void 0;
|
|
674
|
+
runtime.storedBytes = Math.max(0, runtime.storedBytes - bodyBytes(session.request.body));
|
|
675
|
+
session.request.body = "";
|
|
676
|
+
session.entry.state = this.sessionEnabled(session.entry.sessionID) ? "idle" : "off";
|
|
677
|
+
session.entry.nextReplayAt = null;
|
|
678
|
+
session.entry.updatedAt = at;
|
|
679
|
+
this.publish(session);
|
|
680
|
+
}
|
|
681
|
+
enforceStoredBudget(runtime) {
|
|
682
|
+
while (runtime.storedBytes > runtime.maxStoredBytes) {
|
|
683
|
+
const oldestID = [...runtime.sessions.keys()].filter((id) => runtime.sessions.get(id).request.body.length > 0).sort(
|
|
684
|
+
(a, b) => runtime.sessions.get(a).touchedAt - runtime.sessions.get(b).touchedAt
|
|
685
|
+
)[0];
|
|
686
|
+
if (!oldestID) break;
|
|
687
|
+
const session = runtime.sessions.get(oldestID);
|
|
688
|
+
this.clearTimer(session);
|
|
689
|
+
session.controller?.abort();
|
|
690
|
+
runtime.storedBytes -= bodyBytes(session.request.body);
|
|
691
|
+
session.request.body = "";
|
|
692
|
+
runtime.owners.get(oldestID)?.logger.info(`evict session=${oldestID}`);
|
|
693
|
+
session.entry.nextReplayAt = null;
|
|
694
|
+
session.entry.state = "idle";
|
|
695
|
+
session.entry.updatedAt = this.now();
|
|
696
|
+
runtime.owners.get(oldestID)?.publish(session);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
get cap() {
|
|
700
|
+
return this.config.maxReplaysPerGap === "auto" ? Math.floor(
|
|
701
|
+
(this.config.missFactor - this.config.cacheReadFactor) / this.config.cacheReadFactor
|
|
702
|
+
) : this.config.maxReplaysPerGap;
|
|
703
|
+
}
|
|
704
|
+
schedule(id, session, at) {
|
|
705
|
+
this.clearTimer(session);
|
|
706
|
+
if (this.runtime?.owners.get(id) !== this || !session.entry.enabled || session.active || session.entry.state === "stopped" || !session.request.body)
|
|
707
|
+
return;
|
|
708
|
+
session.entry.nextReplayAt = at;
|
|
709
|
+
this.publish(session);
|
|
710
|
+
this.logger.dbg(
|
|
711
|
+
`schedule session=${id} state=${session.busy ? "busy" : "idle"} next=${new Date(at).toISOString()}`
|
|
712
|
+
);
|
|
713
|
+
session.timer = setTimeout(
|
|
714
|
+
() => void this.replay(id, session),
|
|
715
|
+
Math.max(0, at - this.now())
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
async replay(id, session) {
|
|
719
|
+
const runtime = this.runtime;
|
|
720
|
+
if (!runtime || runtime.owners.get(id) !== this || !session.entry.enabled || runtime.sessions.get(id) !== session || session.active)
|
|
721
|
+
return;
|
|
722
|
+
const sentAt = this.now();
|
|
723
|
+
session.request.sentAt = sentAt;
|
|
724
|
+
session.touchedAt = sentAt;
|
|
725
|
+
const controller = new AbortController();
|
|
726
|
+
session.controller = controller;
|
|
727
|
+
session.entry.nextReplayAt = null;
|
|
728
|
+
this.updateState(session, session.busy ? "busy" : "idle");
|
|
729
|
+
let timeout;
|
|
730
|
+
let latencyMs = 0;
|
|
731
|
+
let status = 0;
|
|
401
732
|
try {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
733
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
734
|
+
timeout = setTimeout(() => {
|
|
735
|
+
controller.abort();
|
|
736
|
+
reject(new Error("timeout"));
|
|
737
|
+
}, this.config.replayTimeoutMs);
|
|
738
|
+
});
|
|
739
|
+
const response = await Promise.race([
|
|
740
|
+
runtime.originalFetch(session.request.url, {
|
|
741
|
+
method: session.request.method,
|
|
742
|
+
headers: new Headers(session.request.headers),
|
|
743
|
+
body: session.request.body,
|
|
744
|
+
signal: controller.signal
|
|
745
|
+
}),
|
|
746
|
+
timeoutPromise
|
|
747
|
+
]);
|
|
748
|
+
status = response.status;
|
|
749
|
+
if ([400, 401, 403].includes(status)) {
|
|
750
|
+
session.entry.lastReplay = { at: sentAt, status, latencyMs: this.now() - sentAt };
|
|
751
|
+
this.logger.info(replayLogLine(session, session.entry.lastReplay, "http-status"));
|
|
752
|
+
this.stopSession(id, session, "rejected-4xx", status);
|
|
406
753
|
return;
|
|
407
754
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
this.
|
|
755
|
+
if (!response.ok) {
|
|
756
|
+
session.entry.lastReplay = { at: sentAt, status, latencyMs: this.now() - sentAt };
|
|
757
|
+
this.logger.info(replayLogLine(session, session.entry.lastReplay, "http-status"));
|
|
758
|
+
this.retryOrLapse(id, session, sentAt, void 0, status);
|
|
412
759
|
return;
|
|
413
760
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
);
|
|
761
|
+
const result = await Promise.race([
|
|
762
|
+
readAbortPoint(response, session.request.api, controller),
|
|
763
|
+
timeoutPromise
|
|
764
|
+
]);
|
|
765
|
+
latencyMs = this.now() - sentAt;
|
|
766
|
+
if (session.controller !== controller || runtime.sessions.get(id) !== session) return;
|
|
767
|
+
const { abortEvent, ...usage } = result;
|
|
768
|
+
const last = { at: sentAt, status, latencyMs, ...usage };
|
|
769
|
+
session.entry.lastReplay = last;
|
|
770
|
+
session.entry.gapReplays++;
|
|
771
|
+
session.entry.sessionReplays++;
|
|
772
|
+
session.replayReadTokens += result.cacheRead ?? session.previousPrompt ?? 0;
|
|
773
|
+
this.totals.replays++;
|
|
774
|
+
this.sink.setTotals(this.totals);
|
|
775
|
+
session.retries = 0;
|
|
776
|
+
this.logger.info(replayLogLine(session, last, abortEvent));
|
|
777
|
+
if (session.request.api === "messages" && result.cacheRead === 0 && (result.cacheWrite ?? 0) > 0) {
|
|
778
|
+
session.entry.gapReplays--;
|
|
779
|
+
this.stopSession(id, session, "lapsed");
|
|
434
780
|
return;
|
|
435
781
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
};
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
782
|
+
if (session.entry.gapReplays >= session.entry.cap) {
|
|
783
|
+
this.stopSession(id, session, "cap");
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
this.schedule(id, session, sentAt + session.request.intervalMs);
|
|
787
|
+
} catch (error) {
|
|
788
|
+
if (runtime.sessions.get(id) !== session || session.controller !== controller) return;
|
|
789
|
+
const timeoutHit = this.now() - sentAt >= this.config.replayTimeoutMs;
|
|
790
|
+
session.entry.lastReplay = { at: sentAt, status: 0, latencyMs: this.now() - sentAt };
|
|
791
|
+
this.logger.info(
|
|
792
|
+
replayLogLine(
|
|
793
|
+
session,
|
|
794
|
+
session.entry.lastReplay,
|
|
795
|
+
timeoutHit ? "timeout" : "network"
|
|
796
|
+
)
|
|
448
797
|
);
|
|
449
|
-
|
|
450
|
-
} catch (err) {
|
|
451
|
-
this.logger.warn(`ping ${short(sessionID)} failed`, errText(err));
|
|
798
|
+
this.retryOrLapse(id, session, sentAt, timeoutHit ? "timeout" : "network");
|
|
452
799
|
} finally {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (ping.sent) {
|
|
457
|
-
s.lastPingAt = now;
|
|
458
|
-
const next = s.nextPingAt + jitter(this.config.intervalMs);
|
|
459
|
-
s.nextPingAt = next > now ? next : now + jitter(this.config.intervalMs);
|
|
460
|
-
}
|
|
461
|
-
this.store.persist();
|
|
462
|
-
if (ping.sent && ping.interrupted) {
|
|
463
|
-
s.busy = false;
|
|
464
|
-
s.active = false;
|
|
465
|
-
void this.armWindow(sessionID);
|
|
466
|
-
}
|
|
800
|
+
if (timeout) clearTimeout(timeout);
|
|
801
|
+
controller.abort();
|
|
802
|
+
if (session.controller === controller) session.controller = void 0;
|
|
467
803
|
}
|
|
468
804
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
return (statuses?.[sessionID]?.type ?? "idle") === "idle";
|
|
475
|
-
} catch (err) {
|
|
476
|
-
this.logger.dbg(`status check ${short(sessionID)} failed`, errText(err));
|
|
477
|
-
return false;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
/** Recover the last real turn's request settings when no user event was observed. */
|
|
481
|
-
async lookupRequest(s) {
|
|
482
|
-
const messages = await this.recentMessages(s.sessionID);
|
|
483
|
-
if (!messages) return void 0;
|
|
484
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
485
|
-
const m = messages[i];
|
|
486
|
-
if (m?.info?.role !== "user" || this.isPingMessage(m)) continue;
|
|
487
|
-
const request = requestFrom(m.info);
|
|
488
|
-
if (request) {
|
|
489
|
-
s.request = request;
|
|
490
|
-
return request;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
return void 0;
|
|
805
|
+
retryOrLapse(id, session, sentAt, reason, status = 0) {
|
|
806
|
+
session.entry.lastReplay = { at: sentAt, status, latencyMs: this.now() - sentAt };
|
|
807
|
+
if (session.retries++ === 0) this.schedule(id, session, sentAt + RETRY_MS);
|
|
808
|
+
else this.stopSession(id, session, "lapsed");
|
|
809
|
+
if (reason) this.logger.warn(`replay ${reason} session=${id}`);
|
|
494
810
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const messages = res?.data ?? res;
|
|
502
|
-
return Array.isArray(messages) ? messages : void 0;
|
|
503
|
-
} catch (err) {
|
|
504
|
-
this.logger.warn(`messages ${short(sessionID)} list failed`, errText(err));
|
|
505
|
-
return void 0;
|
|
811
|
+
stopSession(id, session, reason, status) {
|
|
812
|
+
if (session.stopLoggedGap !== session.entry.gapId) {
|
|
813
|
+
session.stopLoggedGap = session.entry.gapId;
|
|
814
|
+
this.logger.info(
|
|
815
|
+
`stop session=${id} reason=${reason}${status === void 0 ? "" : ` status=${status}`}`
|
|
816
|
+
);
|
|
506
817
|
}
|
|
818
|
+
this.clearTimer(session);
|
|
819
|
+
session.controller?.abort();
|
|
820
|
+
session.entry.state = "stopped";
|
|
821
|
+
session.entry.stopReason = reason;
|
|
822
|
+
session.entry.stopStatus = status;
|
|
823
|
+
session.entry.nextReplayAt = null;
|
|
824
|
+
session.entry.updatedAt = this.now();
|
|
825
|
+
if (this.runtime)
|
|
826
|
+
this.runtime.storedBytes = Math.max(
|
|
827
|
+
0,
|
|
828
|
+
this.runtime.storedBytes - bodyBytes(session.request.body)
|
|
829
|
+
);
|
|
830
|
+
session.request.body = "";
|
|
831
|
+
session.request.headers = new Headers();
|
|
832
|
+
this.publish(session);
|
|
507
833
|
}
|
|
508
|
-
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
834
|
+
updateState(session, state) {
|
|
835
|
+
const enabled = this.sessionEnabled(session.entry.sessionID);
|
|
836
|
+
session.entry.state = enabled ? state : "off";
|
|
837
|
+
session.entry.enabled = enabled;
|
|
838
|
+
session.entry.updatedAt = this.now();
|
|
839
|
+
session.entry.cap = this.cap;
|
|
840
|
+
this.publish(session);
|
|
841
|
+
}
|
|
842
|
+
publish(session) {
|
|
843
|
+
this.sink.upsert({ ...session.entry });
|
|
844
|
+
}
|
|
845
|
+
clearTimer(session) {
|
|
846
|
+
if (!session) return;
|
|
847
|
+
if (session.timer) clearTimeout(session.timer);
|
|
848
|
+
session.timer = void 0;
|
|
849
|
+
session.entry.nextReplayAt = null;
|
|
850
|
+
}
|
|
851
|
+
clearTimerFrom(session) {
|
|
852
|
+
if (session.timer) clearTimeout(session.timer);
|
|
853
|
+
session.timer = void 0;
|
|
854
|
+
session.entry.nextReplayAt = null;
|
|
855
|
+
}
|
|
856
|
+
sessionEnabled(sessionID) {
|
|
857
|
+
const instance = this.runtime?.instances.get(this);
|
|
858
|
+
return this.config.enabled && instance?.controls.get(sessionID) !== false;
|
|
859
|
+
}
|
|
860
|
+
disableSession(runtime, id, session) {
|
|
861
|
+
const instance = runtime.instances.get(this);
|
|
862
|
+
instance.effective.set(id, false);
|
|
863
|
+
this.clearTimer(session);
|
|
864
|
+
session.controller?.abort();
|
|
865
|
+
runtime.storedBytes = Math.max(0, runtime.storedBytes - bodyBytes(session.request.body));
|
|
866
|
+
session.request.body = "";
|
|
867
|
+
session.entry.stopReason = "disabled";
|
|
868
|
+
session.entry.state = "off";
|
|
869
|
+
session.entry.enabled = false;
|
|
870
|
+
session.entry.nextReplayAt = null;
|
|
871
|
+
session.entry.updatedAt = this.now();
|
|
872
|
+
if (session.stopLoggedGap !== session.entry.gapId) {
|
|
873
|
+
session.stopLoggedGap = session.entry.gapId;
|
|
874
|
+
this.logger.info(`stop session=${id} reason=disabled`);
|
|
536
875
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
876
|
+
this.publish(session);
|
|
877
|
+
}
|
|
878
|
+
syncControl() {
|
|
879
|
+
const runtime = this.runtime;
|
|
880
|
+
if (!runtime) return;
|
|
881
|
+
const instance = runtime.instances.get(this);
|
|
882
|
+
const controls = readSessionControls(this.directory, this.now()).sessions;
|
|
883
|
+
instance.controls = new Map(
|
|
884
|
+
Object.entries(controls).map(([id, value]) => [id, value.enabled])
|
|
885
|
+
);
|
|
886
|
+
for (const [id, session] of runtime.sessions) {
|
|
887
|
+
if (runtime.owners.get(id) !== this) continue;
|
|
888
|
+
const enabled = this.sessionEnabled(id);
|
|
889
|
+
const previous = instance.effective.get(id) ?? session.entry.enabled;
|
|
890
|
+
instance.effective.set(id, enabled);
|
|
891
|
+
if (previous === enabled) continue;
|
|
892
|
+
if (!enabled) {
|
|
893
|
+
this.disableSession(runtime, id, session);
|
|
894
|
+
} else {
|
|
895
|
+
session.entry.stopReason = void 0;
|
|
896
|
+
session.entry.state = session.active ? "active" : session.busy ? "busy" : "idle";
|
|
897
|
+
session.entry.enabled = true;
|
|
898
|
+
session.entry.updatedAt = this.now();
|
|
899
|
+
this.publish(session);
|
|
900
|
+
}
|
|
544
901
|
}
|
|
545
902
|
}
|
|
546
|
-
|
|
547
|
-
|
|
903
|
+
};
|
|
904
|
+
function getRuntime(now) {
|
|
905
|
+
const target = globalThis;
|
|
906
|
+
if (target[STATE_KEY]) return target[STATE_KEY];
|
|
907
|
+
const runtime = {
|
|
908
|
+
originalFetch: globalThis.fetch,
|
|
909
|
+
sessions: /* @__PURE__ */ new Map(),
|
|
910
|
+
owners: /* @__PURE__ */ new Map(),
|
|
911
|
+
instancesSet: /* @__PURE__ */ new Set(),
|
|
912
|
+
childSessions: /* @__PURE__ */ new Set(),
|
|
913
|
+
loggedApis: /* @__PURE__ */ new Set(),
|
|
914
|
+
now,
|
|
915
|
+
maxStoredBytes: Number.MAX_SAFE_INTEGER,
|
|
916
|
+
storedBytes: 0,
|
|
917
|
+
hosts: /* @__PURE__ */ new Set(),
|
|
918
|
+
instances: /* @__PURE__ */ new Map()
|
|
919
|
+
};
|
|
920
|
+
target[STATE_KEY] = runtime;
|
|
921
|
+
return runtime;
|
|
922
|
+
}
|
|
923
|
+
async function recordRequest(runtime, input, init, bodySource) {
|
|
924
|
+
const headers = new Headers(input instanceof Request ? input.headers : init?.headers);
|
|
925
|
+
if (init?.headers && input instanceof Request)
|
|
926
|
+
new Headers(init.headers).forEach((value, key) => headers.set(key, value));
|
|
927
|
+
const id = headers.get("X-Session-Id") ?? headers.get("x-session-affinity") ?? headers.get("X-Interaction-Id");
|
|
928
|
+
if (!id) return;
|
|
929
|
+
const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
|
|
930
|
+
const rawUrl = input instanceof URL ? input.href : typeof input === "string" ? input : input.url;
|
|
931
|
+
let url;
|
|
932
|
+
try {
|
|
933
|
+
url = new URL(rawUrl);
|
|
934
|
+
} catch {
|
|
935
|
+
return;
|
|
548
936
|
}
|
|
549
|
-
|
|
937
|
+
const mapped = runtime.owners.get(id);
|
|
938
|
+
let owner;
|
|
939
|
+
if (mapped) {
|
|
940
|
+
const state = runtime.instances.get(mapped);
|
|
941
|
+
if (!state?.enabled || state.controls.get(id) === false) return;
|
|
942
|
+
owner = mapped;
|
|
943
|
+
} else {
|
|
944
|
+
const hostname = url.hostname.toLowerCase();
|
|
945
|
+
const eligiblePath = method === "POST" && !!apiFor(url.pathname);
|
|
946
|
+
const candidates = [...runtime.instances].filter(
|
|
947
|
+
([, state]) => state.enabled && state.controls.get(id) !== false && (eligiblePath || runtime.sessions.has(id)) && [...state.hosts].some(
|
|
948
|
+
(allowed) => hostname === allowed || hostname.endsWith(`.${allowed}`)
|
|
949
|
+
)
|
|
950
|
+
);
|
|
951
|
+
if (!candidates.length) return;
|
|
952
|
+
let stringBody;
|
|
550
953
|
try {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
s.modelLabel = info?.model?.id ?? "unknown";
|
|
559
|
-
s.eligible = isEligibleModel(this.config, info?.model?.providerID, info?.model?.id);
|
|
560
|
-
} catch (err) {
|
|
561
|
-
s.eligible = false;
|
|
562
|
-
this.logger.dbg(`resolve ${short(s.sessionID)} failed`, errText(err));
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
pollControl() {
|
|
566
|
-
const control = readControl(this.directory);
|
|
567
|
-
if (!control || control.updatedAt <= this.controlUpdatedAt) return;
|
|
568
|
-
this.controlUpdatedAt = control.updatedAt;
|
|
569
|
-
if (control.enabled === this.enabled) return;
|
|
570
|
-
this.enabled = control.enabled;
|
|
571
|
-
this.store.setEnabled(control.enabled);
|
|
572
|
-
const now = Date.now();
|
|
573
|
-
for (const session of this.store.all()) {
|
|
574
|
-
if (!control.enabled) {
|
|
575
|
-
session.active = false;
|
|
576
|
-
continue;
|
|
577
|
-
}
|
|
578
|
-
if (!session.eligible || session.busy || session.warming) continue;
|
|
579
|
-
if (!session.lastResponseAt || !this.cacheMayBeWarm(session, now)) continue;
|
|
580
|
-
this.openWindow(session, now);
|
|
581
|
-
}
|
|
582
|
-
this.store.persist();
|
|
583
|
-
this.logger.info(`runtime ${control.enabled ? "enabled" : "disabled"}`);
|
|
584
|
-
}
|
|
585
|
-
/** Recover when a session.status idle event is missed by reconciling with the API. */
|
|
586
|
-
async reconcileStatus() {
|
|
587
|
-
if (this.reconcilingStatus) return;
|
|
588
|
-
const busy = this.store.all().filter((session) => session.busy && !session.warming);
|
|
589
|
-
if (busy.length === 0) return;
|
|
590
|
-
this.reconcilingStatus = true;
|
|
954
|
+
stringBody = typeof init?.body === "string" ? init.body : init?.body === void 0 && input instanceof Request ? await (bodySource ?? input.clone().text()) : void 0;
|
|
955
|
+
} catch {
|
|
956
|
+
stringBody = void 0;
|
|
957
|
+
}
|
|
958
|
+
let parsed;
|
|
591
959
|
try {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
960
|
+
if (stringBody !== void 0) parsed = JSON.parse(stringBody);
|
|
961
|
+
} catch {
|
|
962
|
+
parsed = void 0;
|
|
963
|
+
}
|
|
964
|
+
const concurrentOwner = runtime.owners.get(id);
|
|
965
|
+
if (concurrentOwner) {
|
|
966
|
+
const state = runtime.instances.get(concurrentOwner);
|
|
967
|
+
if (!state?.enabled || state.controls.get(id) === false) return;
|
|
968
|
+
await concurrentOwner.recordRequest(runtime, input, init, void 0, stringBody, parsed);
|
|
969
|
+
return;
|
|
602
970
|
}
|
|
971
|
+
const child = runtime.childSessions.has(id);
|
|
972
|
+
const liveCandidates = candidates.filter(
|
|
973
|
+
([instance, state]) => runtime.instances.has(instance) && state.enabled && state.controls.get(id) !== false
|
|
974
|
+
);
|
|
975
|
+
if (!liveCandidates.length) return;
|
|
976
|
+
const fullyEligible = liveCandidates.filter(([, state]) => {
|
|
977
|
+
if (method !== "POST" || !apiFor(url.pathname)) return false;
|
|
978
|
+
if (child && !state.config.includeChildSessions) return false;
|
|
979
|
+
const model = typeof parsed?.model === "string" ? parsed.model.toLowerCase() : "";
|
|
980
|
+
return Object.entries(state.config.intervals).some(
|
|
981
|
+
([key]) => model.includes(key.toLowerCase())
|
|
982
|
+
) && (!child || state.config.includeChildSessions);
|
|
983
|
+
});
|
|
984
|
+
if (!fullyEligible.length) return;
|
|
985
|
+
owner = fullyEligible[0][0];
|
|
986
|
+
runtime.owners.set(id, owner);
|
|
987
|
+
await owner.recordRequest(runtime, input, init, void 0, stringBody, parsed);
|
|
988
|
+
return;
|
|
603
989
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
return Math.max(s.lastResponseAt, s.lastPingAt ?? 0);
|
|
990
|
+
if (!runtime.instances.has(owner) || runtime.owners.get(id) !== owner) return;
|
|
991
|
+
await owner.recordRequest(runtime, input, init, bodySource);
|
|
607
992
|
}
|
|
608
|
-
function
|
|
609
|
-
|
|
610
|
-
const created = info?.time?.created;
|
|
611
|
-
return typeof created !== "number" || created >= (s.lastUserAt ?? 0);
|
|
993
|
+
function bodyBytes(body) {
|
|
994
|
+
return new TextEncoder().encode(body).byteLength;
|
|
612
995
|
}
|
|
613
|
-
function
|
|
614
|
-
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
996
|
+
function pruneOrphanSessions(runtime) {
|
|
997
|
+
const cutoff = runtime.now() - 60 * 6e4;
|
|
998
|
+
for (const [id, session] of runtime.sessions) {
|
|
999
|
+
if (session.request.body || session.touchedAt >= cutoff) continue;
|
|
1000
|
+
session.timer && clearTimeout(session.timer);
|
|
1001
|
+
session.controller?.abort();
|
|
1002
|
+
const owner = runtime.owners.get(id);
|
|
1003
|
+
if (owner) runtime.instances.get(owner)?.sink.remove(id);
|
|
1004
|
+
runtime.owners.delete(id);
|
|
1005
|
+
runtime.sessions.delete(id);
|
|
1006
|
+
}
|
|
619
1007
|
}
|
|
620
|
-
function
|
|
621
|
-
const
|
|
622
|
-
const
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
1008
|
+
function cheapEligibilityRejection(runtime, input, init) {
|
|
1009
|
+
const request = input instanceof Request ? input : void 0;
|
|
1010
|
+
const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
|
|
1011
|
+
const rawUrl = input instanceof URL ? input.href : typeof input === "string" ? input : input.url;
|
|
1012
|
+
let url;
|
|
1013
|
+
try {
|
|
1014
|
+
url = new URL(rawUrl);
|
|
1015
|
+
} catch {
|
|
1016
|
+
return "invalid-url";
|
|
1017
|
+
}
|
|
1018
|
+
const copilotHost = isCopilotHost(url.hostname);
|
|
1019
|
+
const headers = new Headers(request?.headers);
|
|
1020
|
+
if (init?.headers) new Headers(init.headers).forEach((value, key) => headers.set(key, value));
|
|
1021
|
+
const id = headers.get("X-Session-Id") ?? headers.get("x-session-affinity") ?? headers.get("X-Interaction-Id");
|
|
1022
|
+
if (!id) return "missing-session-id";
|
|
1023
|
+
if (/agent-session-(name|summary)-generation/i.test(headers.get("X-Interaction-Type") ?? ""))
|
|
1024
|
+
return "utility-interaction";
|
|
1025
|
+
const hostname = url.hostname.toLowerCase();
|
|
1026
|
+
const owner = runtime.owners.get(id);
|
|
1027
|
+
const allowedHost = owner ? (() => {
|
|
1028
|
+
const state = runtime.instances.get(owner);
|
|
1029
|
+
return !!state?.enabled && state.controls.get(id) !== false && [...state.hosts].some(
|
|
1030
|
+
(allowed) => hostname === allowed || hostname.endsWith(`.${allowed}`)
|
|
1031
|
+
);
|
|
1032
|
+
})() : [...runtime.instances.values()].some(
|
|
1033
|
+
(state) => state.enabled && state.controls.get(id) !== false && [...state.hosts].some(
|
|
1034
|
+
(allowed) => hostname === allowed || hostname.endsWith(`.${allowed}`)
|
|
1035
|
+
)
|
|
1036
|
+
);
|
|
1037
|
+
if (!allowedHost) return "host-not-allowed";
|
|
1038
|
+
if (runtime.owners.has(id) || method === "POST" && !!apiFor(url.pathname)) return void 0;
|
|
1039
|
+
if (!copilotHost) return "ineligible";
|
|
1040
|
+
return method !== "POST" ? "method-not-post" : "unsupported-path";
|
|
1041
|
+
}
|
|
1042
|
+
function isReplayRequest(input, init) {
|
|
1043
|
+
const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
|
|
1044
|
+
const rawUrl = input instanceof URL ? input.href : typeof input === "string" ? input : input.url;
|
|
1045
|
+
try {
|
|
1046
|
+
return method === "POST" && !!apiFor(new URL(rawUrl).pathname);
|
|
1047
|
+
} catch {
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
632
1050
|
}
|
|
633
|
-
function
|
|
634
|
-
|
|
1051
|
+
function isCopilotHost(hostname) {
|
|
1052
|
+
const host = hostname.toLowerCase();
|
|
1053
|
+
return host === "githubcopilot.com" || host.endsWith(".githubcopilot.com") || host.startsWith("copilot-api.");
|
|
635
1054
|
}
|
|
636
|
-
function
|
|
637
|
-
|
|
1055
|
+
function isCopilotRequest(input) {
|
|
1056
|
+
const rawUrl = input instanceof URL ? input.href : typeof input === "string" ? input : input.url;
|
|
1057
|
+
try {
|
|
1058
|
+
return isCopilotHost(new URL(rawUrl).hostname);
|
|
1059
|
+
} catch {
|
|
1060
|
+
return false;
|
|
1061
|
+
}
|
|
638
1062
|
}
|
|
639
|
-
function
|
|
640
|
-
|
|
1063
|
+
function apiFor(path) {
|
|
1064
|
+
if (/\/v1\/messages\/?$/.test(path)) return "messages";
|
|
1065
|
+
if (/^\/(?:v1\/)?responses\/?$/.test(path)) return "responses";
|
|
1066
|
+
if (/\/chat\/completions\/?$/.test(path)) return "chat";
|
|
1067
|
+
}
|
|
1068
|
+
async function readAbortPoint(response, api, controller) {
|
|
1069
|
+
const reader = response.body?.getReader();
|
|
1070
|
+
if (!reader) {
|
|
1071
|
+
controller.abort();
|
|
1072
|
+
return { abortEvent: "first-chunk" };
|
|
1073
|
+
}
|
|
1074
|
+
let buffer = "";
|
|
1075
|
+
try {
|
|
1076
|
+
while (true) {
|
|
1077
|
+
const { value, done } = await reader.read();
|
|
1078
|
+
if (done) break;
|
|
1079
|
+
if (!value) continue;
|
|
1080
|
+
if (api === "chat") {
|
|
1081
|
+
controller.abort();
|
|
1082
|
+
return { abortEvent: "first-chunk" };
|
|
1083
|
+
}
|
|
1084
|
+
buffer += new TextDecoder().decode(value, { stream: true });
|
|
1085
|
+
const events = buffer.split(/\r?\n\r?\n/);
|
|
1086
|
+
buffer = events.pop() ?? "";
|
|
1087
|
+
for (const event of events) {
|
|
1088
|
+
const data = event.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("\n");
|
|
1089
|
+
if (!data || data === "[DONE]") continue;
|
|
1090
|
+
let parsed;
|
|
1091
|
+
try {
|
|
1092
|
+
parsed = JSON.parse(data);
|
|
1093
|
+
} catch {
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
if (api === "responses" && ["response.created", "response.in_progress"].includes(parsed.type))
|
|
1097
|
+
continue;
|
|
1098
|
+
if (api === "messages") {
|
|
1099
|
+
if (parsed.type !== "message_start") continue;
|
|
1100
|
+
const usage = parsed.message?.usage ?? {};
|
|
1101
|
+
controller.abort();
|
|
1102
|
+
return {
|
|
1103
|
+
abortEvent: "message_start",
|
|
1104
|
+
cacheRead: numeric(usage.cache_read_input_tokens),
|
|
1105
|
+
cacheWrite: numeric(usage.cache_creation_input_tokens)
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
controller.abort();
|
|
1109
|
+
return { abortEvent: String(parsed.type ?? "first-event") };
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
} finally {
|
|
1113
|
+
controller.abort();
|
|
1114
|
+
await reader.cancel().catch(() => {
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
return { abortEvent: "stream-end" };
|
|
1118
|
+
}
|
|
1119
|
+
function replayLogLine(session, result, abortEvent) {
|
|
1120
|
+
const usage = result.cacheRead === void 0 && result.cacheWrite === void 0 ? "" : ` cacheRead=${result.cacheRead ?? 0} cacheWrite=${result.cacheWrite ?? 0}`;
|
|
1121
|
+
return `replay session=${session.entry.sessionID} model=${session.entry.model} api=${session.entry.api} gap=${session.entry.gapId} n=${session.entry.gapReplays}/${session.entry.cap} status=${result.status} latency=${result.latencyMs}ms abort=${abortEvent ?? "first-chunk"}${usage}`;
|
|
1122
|
+
}
|
|
1123
|
+
function numeric(value) {
|
|
1124
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
641
1125
|
}
|
|
642
1126
|
|
|
643
1127
|
// lib/logger.ts
|
|
@@ -681,88 +1165,114 @@ var Logger = class {
|
|
|
681
1165
|
};
|
|
682
1166
|
|
|
683
1167
|
// lib/state.ts
|
|
684
|
-
import {
|
|
685
|
-
import {
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
directory;
|
|
694
|
-
sessions = /* @__PURE__ */ new Map();
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
lastResponseAt: 0,
|
|
710
|
-
idleSince: 0,
|
|
711
|
-
windowEndsAt: 0,
|
|
712
|
-
nextPingAt: 0,
|
|
713
|
-
pingsSent: 0,
|
|
714
|
-
busy: false,
|
|
715
|
-
warming: false,
|
|
716
|
-
active: false
|
|
717
|
-
};
|
|
718
|
-
this.sessions.set(sessionID, created);
|
|
719
|
-
return created;
|
|
720
|
-
}
|
|
721
|
-
remove(sessionID) {
|
|
722
|
-
this.sessions.delete(sessionID);
|
|
723
|
-
}
|
|
724
|
-
all() {
|
|
725
|
-
return [...this.sessions.values()];
|
|
726
|
-
}
|
|
727
|
-
/**
|
|
728
|
-
* Atomically persist a lean snapshot for the TUI. Never throws or rejects; the
|
|
729
|
-
* returned promise resolves once this snapshot and every earlier one are written.
|
|
730
|
-
*/
|
|
731
|
-
persist() {
|
|
1168
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1169
|
+
import { readdirSync, readFileSync as readFileSync2, unlinkSync } from "fs";
|
|
1170
|
+
import { mkdir as mkdir2, rename as rename2, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
1171
|
+
import { dirname as dirname3, join as join2 } from "path";
|
|
1172
|
+
var CLEANUP_INTERVAL_MS = 10 * 6e4;
|
|
1173
|
+
var PERSIST_DEBOUNCE_MS = 250;
|
|
1174
|
+
function createStateStore(directory, options = {}) {
|
|
1175
|
+
const now = options.now ?? Date.now;
|
|
1176
|
+
const instanceId = randomUUID2();
|
|
1177
|
+
const filePath = instanceStateFilePath(directory, process.pid, instanceId);
|
|
1178
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
1179
|
+
let totals = {
|
|
1180
|
+
replays: 0,
|
|
1181
|
+
avoidedTokens: 0,
|
|
1182
|
+
replayReadTokens: 0,
|
|
1183
|
+
resumeHits: 0,
|
|
1184
|
+
resumeMisses: 0
|
|
1185
|
+
};
|
|
1186
|
+
let timer;
|
|
1187
|
+
let cleanupTimer;
|
|
1188
|
+
let writeQueue = Promise.resolve();
|
|
1189
|
+
let disposed = false;
|
|
1190
|
+
const flush = () => {
|
|
1191
|
+
if (timer) clearTimeout(timer);
|
|
1192
|
+
timer = void 0;
|
|
732
1193
|
const snapshot = {
|
|
733
|
-
version:
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
this.all().map((s) => [
|
|
740
|
-
s.sessionID,
|
|
741
|
-
{
|
|
742
|
-
eligible: s.eligible,
|
|
743
|
-
modelLabel: s.modelLabel,
|
|
744
|
-
lastResponseAt: s.lastResponseAt,
|
|
745
|
-
idleSince: s.idleSince,
|
|
746
|
-
windowEndsAt: s.windowEndsAt,
|
|
747
|
-
nextPingAt: s.nextPingAt,
|
|
748
|
-
intervalMs: this.config.intervalMs,
|
|
749
|
-
pingsSent: s.pingsSent,
|
|
750
|
-
busy: s.busy,
|
|
751
|
-
warming: s.warming,
|
|
752
|
-
active: s.active,
|
|
753
|
-
lastPing: s.lastPing
|
|
754
|
-
}
|
|
755
|
-
])
|
|
756
|
-
)
|
|
1194
|
+
version: 2,
|
|
1195
|
+
pid: process.pid,
|
|
1196
|
+
instanceId,
|
|
1197
|
+
updatedAt: now(),
|
|
1198
|
+
sessions: Object.fromEntries(sessions),
|
|
1199
|
+
totals
|
|
757
1200
|
};
|
|
758
|
-
const write = () => atomicWrite(
|
|
759
|
-
|
|
760
|
-
return
|
|
1201
|
+
const write = () => atomicWrite(filePath, JSON.stringify(snapshot));
|
|
1202
|
+
writeQueue = writeQueue.then(write, write);
|
|
1203
|
+
return writeQueue;
|
|
1204
|
+
};
|
|
1205
|
+
const schedule = () => {
|
|
1206
|
+
if (disposed || timer) return;
|
|
1207
|
+
timer = setTimeout(() => {
|
|
1208
|
+
timer = void 0;
|
|
1209
|
+
void flush();
|
|
1210
|
+
}, PERSIST_DEBOUNCE_MS);
|
|
1211
|
+
};
|
|
1212
|
+
const onExit = () => {
|
|
1213
|
+
try {
|
|
1214
|
+
unlinkSync(filePath);
|
|
1215
|
+
} catch {
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
cleanupStateFiles(directory, options.logger);
|
|
1219
|
+
cleanupTimer = setInterval(
|
|
1220
|
+
() => cleanupStateFiles(directory, options.logger),
|
|
1221
|
+
CLEANUP_INTERVAL_MS
|
|
1222
|
+
);
|
|
1223
|
+
process.on("exit", onExit);
|
|
1224
|
+
return {
|
|
1225
|
+
upsert(entry) {
|
|
1226
|
+
if (disposed) return;
|
|
1227
|
+
sessions.set(entry.sessionID, { ...entry });
|
|
1228
|
+
schedule();
|
|
1229
|
+
},
|
|
1230
|
+
remove(sessionID) {
|
|
1231
|
+
if (disposed) return;
|
|
1232
|
+
sessions.delete(sessionID);
|
|
1233
|
+
schedule();
|
|
1234
|
+
},
|
|
1235
|
+
setTotals(value) {
|
|
1236
|
+
if (disposed) return;
|
|
1237
|
+
totals = { ...value };
|
|
1238
|
+
schedule();
|
|
1239
|
+
},
|
|
1240
|
+
async dispose() {
|
|
1241
|
+
if (disposed) return;
|
|
1242
|
+
disposed = true;
|
|
1243
|
+
if (timer) clearTimeout(timer);
|
|
1244
|
+
timer = void 0;
|
|
1245
|
+
if (cleanupTimer) clearInterval(cleanupTimer);
|
|
1246
|
+
cleanupTimer = void 0;
|
|
1247
|
+
process.off("exit", onExit);
|
|
1248
|
+
await flush();
|
|
1249
|
+
await unlink(filePath).catch(() => {
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
function cleanupStateFiles(directory, logger) {
|
|
1255
|
+
try {
|
|
1256
|
+
const prefix = `state-v2-${stateDirectoryKey(directory)}-`;
|
|
1257
|
+
for (const name of readdirSync(stateDirectoryPath())) {
|
|
1258
|
+
const match = name.match(new RegExp(`^${prefix}(\\d+)-[^/]+\\.json$`));
|
|
1259
|
+
if (!match) continue;
|
|
1260
|
+
const pid = Number(match[1]);
|
|
1261
|
+
try {
|
|
1262
|
+
process.kill(pid, 0);
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
if (error.code === "ESRCH") {
|
|
1265
|
+
unlinkSync(join2(stateDirectoryPath(), name));
|
|
1266
|
+
logger?.dbg("Removed stale state snapshot", { pid });
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
} catch {
|
|
761
1271
|
}
|
|
762
|
-
}
|
|
1272
|
+
}
|
|
763
1273
|
async function atomicWrite(path, contents) {
|
|
764
1274
|
try {
|
|
765
|
-
const tmp = `${path}.${process.pid}.${
|
|
1275
|
+
const tmp = `${path}.${process.pid}.${randomUUID2()}.tmp`;
|
|
766
1276
|
await mkdir2(dirname3(path), { recursive: true });
|
|
767
1277
|
await writeFile2(tmp, `${contents}
|
|
768
1278
|
`, "utf8");
|
|
@@ -771,54 +1281,27 @@ async function atomicWrite(path, contents) {
|
|
|
771
1281
|
}
|
|
772
1282
|
}
|
|
773
1283
|
|
|
774
|
-
// lib/system.ts
|
|
775
|
-
function keepaliveInstruction(pingToken) {
|
|
776
|
-
return [
|
|
777
|
-
"<cache_keepalive>",
|
|
778
|
-
`A user message consisting of exactly the single token "${pingToken}" is an`,
|
|
779
|
-
"automated cache-keepalive ping, not a real request. When you receive it,",
|
|
780
|
-
`reply with exactly "${pingToken}" and nothing else. Do not call any tools,`,
|
|
781
|
-
"do not explain, and do not take any action.",
|
|
782
|
-
"</cache_keepalive>"
|
|
783
|
-
].join("\n");
|
|
784
|
-
}
|
|
785
|
-
|
|
786
1284
|
// index.ts
|
|
787
1285
|
var server = async (ctx, options) => {
|
|
788
|
-
const
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
logger.
|
|
792
|
-
|
|
793
|
-
|
|
1286
|
+
const logger = new Logger(false, logFilePath());
|
|
1287
|
+
const config = getConfig(
|
|
1288
|
+
options,
|
|
1289
|
+
(message) => logger.warn(message)
|
|
1290
|
+
);
|
|
1291
|
+
const activeLogger = new Logger(config.debug, logFilePath());
|
|
794
1292
|
if (isSecureMode()) configureClientAuth(ctx.client);
|
|
795
|
-
const store =
|
|
796
|
-
const engine = new
|
|
1293
|
+
const store = createStateStore(ctx.directory, { logger: activeLogger });
|
|
1294
|
+
const engine = new ReplayWarmer(ctx.client, config, store, activeLogger, ctx.directory);
|
|
797
1295
|
engine.start();
|
|
798
|
-
const version = true ? "0.
|
|
1296
|
+
const version = true ? "0.2.0" : "dev";
|
|
799
1297
|
logger.info(`v${version} ready`);
|
|
800
|
-
const instruction = keepaliveInstruction(config.pingToken);
|
|
801
1298
|
return {
|
|
802
1299
|
event: async ({ event }) => {
|
|
803
|
-
|
|
804
|
-
},
|
|
805
|
-
// Keep the system prefix identical between real turns and pings, and make
|
|
806
|
-
// the model answer a ping with a single token.
|
|
807
|
-
"experimental.chat.system.transform": async (input, output) => {
|
|
808
|
-
if (!config.injectSystemInstruction) return;
|
|
809
|
-
const model = input.model;
|
|
810
|
-
if (!isEligibleModel(config, model?.providerID, model?.id ?? model?.modelID)) return;
|
|
811
|
-
output.system.push(instruction);
|
|
812
|
-
},
|
|
813
|
-
// Backstop: if the model ignores the instruction and tries a tool during a
|
|
814
|
-
// ping, block it — the ping turn is going to be reverted anyway.
|
|
815
|
-
"tool.execute.before": async (input) => {
|
|
816
|
-
if (engine.shouldBlockTools(input.sessionID)) {
|
|
817
|
-
throw new Error("cache-keepalive: tools are disabled during a keepalive ping");
|
|
818
|
-
}
|
|
1300
|
+
engine.onEvent(event);
|
|
819
1301
|
},
|
|
820
1302
|
dispose: async () => {
|
|
821
|
-
engine.
|
|
1303
|
+
engine.dispose();
|
|
1304
|
+
await store.dispose();
|
|
822
1305
|
}
|
|
823
1306
|
};
|
|
824
1307
|
};
|