@vikrant82/opencode-cache-keepalive 0.1.6 → 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 -51
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1093 -649
- 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 -37
- 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 -117
- package/dist/lib/state.d.ts.map +1 -1
- package/dist/lib/tui/commands.d.ts +1 -6
- 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.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 +50 -86
- package/lib/paths.ts +12 -14
- package/lib/replay-warmer.ts +1200 -0
- package/lib/state.ts +171 -181
- package/lib/tui/commands.tsx +36 -144
- package/lib/tui/footer.tsx +54 -156
- package/lib/tui/format.ts +4 -0
- package/lib/types.ts +130 -0
- package/package.json +3 -2
- package/tui.tsx +40 -0
- package/dist/lib/keepalive.d.ts +0 -88
- 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 -665
- package/lib/model.ts +0 -19
- package/lib/system.ts +0 -19
package/dist/index.js
CHANGED
|
@@ -25,66 +25,99 @@ 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
|
|
@@ -97,9 +130,14 @@ import { dirname } from "path";
|
|
|
97
130
|
import { createHash } from "crypto";
|
|
98
131
|
import { homedir } from "os";
|
|
99
132
|
import { join } from "path";
|
|
100
|
-
function
|
|
101
|
-
|
|
102
|
-
|
|
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
|
+
);
|
|
103
141
|
}
|
|
104
142
|
function stateDirectoryPath() {
|
|
105
143
|
return join(
|
|
@@ -119,558 +157,971 @@ function logFilePath() {
|
|
|
119
157
|
}
|
|
120
158
|
|
|
121
159
|
// lib/control.ts
|
|
122
|
-
var
|
|
123
|
-
var
|
|
124
|
-
function
|
|
125
|
-
return typeof ms === "number" && Number.isFinite(ms) && ms >= MIN_INTERVAL_MS && ms <= MAX_INTERVAL_MS;
|
|
126
|
-
}
|
|
127
|
-
function readControl(directory) {
|
|
160
|
+
var VERSION = 2;
|
|
161
|
+
var MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
162
|
+
function readSessionControls(directory, now = Date.now()) {
|
|
128
163
|
try {
|
|
129
164
|
const value = JSON.parse(readFileSync(controlFilePath(directory), "utf8"));
|
|
130
|
-
if (value
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
updatedAt
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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 };
|
|
138
177
|
} catch {
|
|
139
|
-
return
|
|
178
|
+
return { version: VERSION, sessions: {} };
|
|
140
179
|
}
|
|
141
180
|
}
|
|
142
181
|
var updateQueue = Promise.resolve();
|
|
143
182
|
|
|
144
|
-
// lib/
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const providerOk = config.providerAllowlist.some((needle) => provider.includes(needle));
|
|
150
|
-
const modelOk = config.modelAllowlist.some((needle) => model.includes(needle));
|
|
151
|
-
return providerOk && modelOk;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// lib/keepalive.ts
|
|
155
|
-
var TICK_MS = 15e3;
|
|
156
|
-
var JITTER_MS = 15e3;
|
|
157
|
-
var CONTROL_POLL_MS = 1e3;
|
|
158
|
-
var STATUS_POLL_MS = 2e3;
|
|
159
|
-
var PING_STATUS_GRACE_MS = 5e3;
|
|
160
|
-
var COLD_AFTER_INTERVALS = 2;
|
|
161
|
-
var MESSAGE_LOOKUP_LIMIT = 50;
|
|
162
|
-
var KeepaliveEngine = class {
|
|
163
|
-
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) {
|
|
164
188
|
this.client = client;
|
|
165
189
|
this.config = config;
|
|
166
|
-
this.
|
|
190
|
+
this.sink = sink;
|
|
167
191
|
this.logger = logger;
|
|
168
192
|
this.directory = directory;
|
|
169
|
-
|
|
170
|
-
this.enabled = control?.enabled ?? config.enabled;
|
|
171
|
-
this.intervalOverride = control?.intervalMs;
|
|
172
|
-
this.controlUpdatedAt = control?.updatedAt ?? 0;
|
|
173
|
-
this.store.setEnabled(this.enabled);
|
|
174
|
-
this.store.setIntervalMs(this.intervalMs);
|
|
193
|
+
this.now = now;
|
|
175
194
|
}
|
|
176
195
|
client;
|
|
177
196
|
config;
|
|
178
|
-
|
|
197
|
+
sink;
|
|
179
198
|
logger;
|
|
180
199
|
directory;
|
|
181
|
-
|
|
200
|
+
now;
|
|
201
|
+
runtime;
|
|
182
202
|
controlTimer;
|
|
183
|
-
|
|
184
|
-
controlUpdatedAt = 0;
|
|
185
|
-
reconcilingStatus = false;
|
|
186
|
-
enabled;
|
|
187
|
-
intervalOverride;
|
|
188
|
-
/** Effective ping interval: the runtime override from the control file, else config. */
|
|
189
|
-
get intervalMs() {
|
|
190
|
-
return this.intervalOverride ?? this.config.intervalMs;
|
|
191
|
-
}
|
|
203
|
+
enabled = false;
|
|
192
204
|
start() {
|
|
193
|
-
if (this.
|
|
194
|
-
|
|
195
|
-
this.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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);
|
|
201
259
|
}
|
|
202
|
-
|
|
203
|
-
if (!this.timer) return;
|
|
204
|
-
clearInterval(this.timer);
|
|
205
|
-
this.timer = void 0;
|
|
260
|
+
dispose() {
|
|
206
261
|
if (this.controlTimer) clearInterval(this.controlTimer);
|
|
207
262
|
this.controlTimer = void 0;
|
|
208
|
-
|
|
209
|
-
this.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
+
});
|
|
228
327
|
}
|
|
229
328
|
return;
|
|
230
329
|
}
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
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);
|
|
237
335
|
return;
|
|
238
336
|
}
|
|
239
|
-
if (
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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");
|
|
247
362
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
s.active = false;
|
|
257
|
-
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());
|
|
258
371
|
return;
|
|
259
372
|
}
|
|
260
|
-
if (
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
this.
|
|
373
|
+
if (info2.role === "assistant" && info2.time?.completed === void 0) {
|
|
374
|
+
session.active = true;
|
|
375
|
+
this.clearTimer(session);
|
|
376
|
+
this.updateState(session, "active");
|
|
264
377
|
}
|
|
265
378
|
return;
|
|
266
379
|
}
|
|
267
|
-
if (type
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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;
|
|
277
418
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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);
|
|
437
|
+
}
|
|
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");
|
|
282
471
|
return;
|
|
283
472
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (this.
|
|
288
|
-
await this.armWindow(sessionID);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Track a new user message observed while a ping is in flight. Event order alone
|
|
293
|
-
* cannot tell the ping's own message from a real prompt racing it, so messages are
|
|
294
|
-
* held until their text part arrives (see `notePingRunPart`). Any message seen
|
|
295
|
-
* before the ping is sent, or a second new message, is necessarily a real turn.
|
|
296
|
-
*/
|
|
297
|
-
notePingRunUser(s, info) {
|
|
298
|
-
const ping = s.ping;
|
|
299
|
-
if (!ping || typeof info?.id !== "string") return;
|
|
300
|
-
const created = info?.time?.created;
|
|
301
|
-
if (typeof created === "number" && created < ping.startedAt) return;
|
|
302
|
-
if (info.id === ping.messageID || ping.pending.has(info.id)) return;
|
|
303
|
-
if (!ping.sent || ping.messageID) {
|
|
304
|
-
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");
|
|
305
477
|
return;
|
|
306
478
|
}
|
|
307
|
-
|
|
308
|
-
if (
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Classify a pending user message by its first text part: the ping token marks
|
|
312
|
-
* the ping's own message; any other text is a real turn joining the ping run.
|
|
313
|
-
* opencode publishes a user message's parts right after the message itself, before
|
|
314
|
-
* the run reaches any tool call.
|
|
315
|
-
*/
|
|
316
|
-
notePingRunPart(s, part) {
|
|
317
|
-
const ping = s.ping;
|
|
318
|
-
if (!ping || part?.type !== "text" || part?.synthetic) return;
|
|
319
|
-
const id = part?.messageID;
|
|
320
|
-
if (typeof id !== "string" || !ping.pending.has(id)) return;
|
|
321
|
-
const info = ping.pending.get(id);
|
|
322
|
-
ping.pending.delete(id);
|
|
323
|
-
if (!ping.messageID && String(part.text ?? "").trim() === this.config.pingToken) {
|
|
324
|
-
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");
|
|
325
482
|
return;
|
|
326
483
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (!ping) return;
|
|
333
|
-
if (!ping.interrupted) this.logger.dbg(`real turn joined ping ${short(s.sessionID)}`);
|
|
334
|
-
ping.interrupted = true;
|
|
335
|
-
if (info) noteUserMessage(s, info);
|
|
336
|
-
s.busy = true;
|
|
337
|
-
s.active = false;
|
|
338
|
-
this.store.persist();
|
|
339
|
-
}
|
|
340
|
-
async armWindow(sessionID) {
|
|
341
|
-
const s = this.store.ensure(sessionID);
|
|
342
|
-
if (s.active && !s.busy) return;
|
|
343
|
-
if (s.arming) return;
|
|
344
|
-
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;
|
|
345
489
|
try {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
+
};
|
|
352
514
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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);
|
|
359
526
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
s.windowEndsAt = s.lastResponseAt + this.config.windowMs;
|
|
371
|
-
s.nextPingAt = Math.max(now, lastCacheTouch(s) + this.intervalMs);
|
|
372
|
-
s.pingsSent = 0;
|
|
373
|
-
s.lastPing = void 0;
|
|
374
|
-
s.active = true;
|
|
375
|
-
}
|
|
376
|
-
/** False once the cache has gone too long without a request to still be trusted warm. */
|
|
377
|
-
cacheMayBeWarm(s, now) {
|
|
378
|
-
const touch = lastCacheTouch(s);
|
|
379
|
-
return touch > 0 && now - touch <= COLD_AFTER_INTERVALS * this.intervalMs;
|
|
380
|
-
}
|
|
381
|
-
async tick() {
|
|
382
|
-
if (!this.enabled) return;
|
|
383
|
-
const now = Date.now();
|
|
384
|
-
for (const s of this.store.all()) {
|
|
385
|
-
if (!s.eligible || !s.active || s.busy || s.warming) continue;
|
|
386
|
-
if (now >= s.windowEndsAt) {
|
|
387
|
-
s.active = false;
|
|
388
|
-
this.store.persist();
|
|
389
|
-
this.logger.dbg(
|
|
390
|
-
`window closed ${short(s.sessionID)} after ${s.pingsSent} ping(s) \u2014 cache may cool`
|
|
391
|
-
);
|
|
392
|
-
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);
|
|
393
537
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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"
|
|
400
557
|
);
|
|
401
|
-
|
|
558
|
+
if (session) {
|
|
559
|
+
this.invalidateTarget(runtime, session, sentAt);
|
|
402
560
|
}
|
|
403
|
-
|
|
561
|
+
return;
|
|
404
562
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
|
414
642
|
};
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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;
|
|
418
732
|
try {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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);
|
|
423
753
|
return;
|
|
424
754
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
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);
|
|
429
759
|
return;
|
|
430
760
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
);
|
|
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");
|
|
451
780
|
return;
|
|
452
781
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
};
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
+
)
|
|
465
797
|
);
|
|
466
|
-
|
|
467
|
-
} catch (err) {
|
|
468
|
-
this.logger.warn(`ping ${short(sessionID)} failed`, errText(err));
|
|
798
|
+
this.retryOrLapse(id, session, sentAt, timeoutHit ? "timeout" : "network");
|
|
469
799
|
} finally {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if (ping.sent) {
|
|
474
|
-
s.lastPingAt = now;
|
|
475
|
-
const next = s.nextPingAt + jitter(this.intervalMs);
|
|
476
|
-
s.nextPingAt = next > now ? next : now + jitter(this.intervalMs);
|
|
477
|
-
}
|
|
478
|
-
this.store.persist();
|
|
479
|
-
if (ping.sent && ping.interrupted) {
|
|
480
|
-
s.busy = false;
|
|
481
|
-
s.active = false;
|
|
482
|
-
void this.armWindow(sessionID);
|
|
483
|
-
}
|
|
800
|
+
if (timeout) clearTimeout(timeout);
|
|
801
|
+
controller.abort();
|
|
802
|
+
if (session.controller === controller) session.controller = void 0;
|
|
484
803
|
}
|
|
485
804
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
return (statuses?.[sessionID]?.type ?? "idle") === "idle";
|
|
492
|
-
} catch (err) {
|
|
493
|
-
this.logger.dbg(`status check ${short(sessionID)} failed`, errText(err));
|
|
494
|
-
return false;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
/** Recover the last real turn's request settings when no user event was observed. */
|
|
498
|
-
async lookupRequest(s) {
|
|
499
|
-
const messages = await this.recentMessages(s.sessionID);
|
|
500
|
-
if (!messages) return void 0;
|
|
501
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
502
|
-
const m = messages[i];
|
|
503
|
-
if (m?.info?.role !== "user" || this.isPingMessage(m)) continue;
|
|
504
|
-
const request = requestFrom(m.info);
|
|
505
|
-
if (request) {
|
|
506
|
-
s.request = request;
|
|
507
|
-
return request;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
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}`);
|
|
511
810
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
const messages = res?.data ?? res;
|
|
519
|
-
return Array.isArray(messages) ? messages : void 0;
|
|
520
|
-
} catch (err) {
|
|
521
|
-
this.logger.warn(`messages ${short(sessionID)} list failed`, errText(err));
|
|
522
|
-
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
|
+
);
|
|
523
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);
|
|
524
833
|
}
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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`);
|
|
553
875
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
+
}
|
|
561
901
|
}
|
|
562
902
|
}
|
|
563
|
-
|
|
564
|
-
|
|
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;
|
|
565
936
|
}
|
|
566
|
-
|
|
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;
|
|
567
953
|
try {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
s.eligible = false;
|
|
572
|
-
s.modelLabel = info?.model?.id ?? "child";
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
s.modelLabel = info?.model?.id ?? "unknown";
|
|
576
|
-
s.eligible = isEligibleModel(this.config, info?.model?.providerID, info?.model?.id);
|
|
577
|
-
} catch (err) {
|
|
578
|
-
s.eligible = false;
|
|
579
|
-
this.logger.dbg(`resolve ${short(s.sessionID)} failed`, errText(err));
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
pollControl() {
|
|
583
|
-
const control = readControl(this.directory);
|
|
584
|
-
if (!control || control.updatedAt <= this.controlUpdatedAt) return;
|
|
585
|
-
this.controlUpdatedAt = control.updatedAt;
|
|
586
|
-
const now = Date.now();
|
|
587
|
-
let changed = false;
|
|
588
|
-
if (control.intervalMs !== this.intervalOverride) {
|
|
589
|
-
this.intervalOverride = control.intervalMs;
|
|
590
|
-
this.store.setIntervalMs(this.intervalMs);
|
|
591
|
-
for (const session of this.store.all()) {
|
|
592
|
-
if (session.active && !session.warming)
|
|
593
|
-
session.nextPingAt = Math.max(now, lastCacheTouch(session) + this.intervalMs);
|
|
594
|
-
}
|
|
595
|
-
this.logger.info(
|
|
596
|
-
`runtime interval ${Math.round(this.intervalMs / 1e3)}s` + (control.intervalMs === void 0 ? " (config)" : "")
|
|
597
|
-
);
|
|
598
|
-
changed = true;
|
|
599
|
-
}
|
|
600
|
-
const enabled = control.enabled ?? this.config.enabled;
|
|
601
|
-
if (enabled !== this.enabled) {
|
|
602
|
-
this.enabled = enabled;
|
|
603
|
-
this.store.setEnabled(enabled);
|
|
604
|
-
for (const session of this.store.all()) {
|
|
605
|
-
if (!enabled) {
|
|
606
|
-
session.active = false;
|
|
607
|
-
continue;
|
|
608
|
-
}
|
|
609
|
-
if (!session.eligible || session.busy || session.warming) continue;
|
|
610
|
-
if (!session.lastResponseAt || !this.cacheMayBeWarm(session, now)) continue;
|
|
611
|
-
this.openWindow(session, now);
|
|
612
|
-
}
|
|
613
|
-
this.logger.info(`runtime ${enabled ? "enabled" : "disabled"}`);
|
|
614
|
-
changed = 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;
|
|
615
957
|
}
|
|
616
|
-
|
|
617
|
-
}
|
|
618
|
-
/** Recover when a session.status idle event is missed by reconciling with the API. */
|
|
619
|
-
async reconcileStatus() {
|
|
620
|
-
if (this.reconcilingStatus) return;
|
|
621
|
-
const busy = this.store.all().filter((session) => session.busy && !session.warming);
|
|
622
|
-
if (busy.length === 0) return;
|
|
623
|
-
this.reconcilingStatus = true;
|
|
958
|
+
let parsed;
|
|
624
959
|
try {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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;
|
|
635
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;
|
|
636
989
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
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);
|
|
640
992
|
}
|
|
641
|
-
function
|
|
642
|
-
|
|
643
|
-
const created = info?.time?.created;
|
|
644
|
-
return typeof created !== "number" || created >= (s.lastUserAt ?? 0);
|
|
993
|
+
function bodyBytes(body) {
|
|
994
|
+
return new TextEncoder().encode(body).byteLength;
|
|
645
995
|
}
|
|
646
|
-
function
|
|
647
|
-
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
+
}
|
|
652
1007
|
}
|
|
653
|
-
function
|
|
654
|
-
const
|
|
655
|
-
const
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
+
}
|
|
665
1050
|
}
|
|
666
|
-
function
|
|
667
|
-
|
|
1051
|
+
function isCopilotHost(hostname) {
|
|
1052
|
+
const host = hostname.toLowerCase();
|
|
1053
|
+
return host === "githubcopilot.com" || host.endsWith(".githubcopilot.com") || host.startsWith("copilot-api.");
|
|
668
1054
|
}
|
|
669
|
-
function
|
|
670
|
-
|
|
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
|
+
}
|
|
1062
|
+
}
|
|
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" };
|
|
671
1118
|
}
|
|
672
|
-
function
|
|
673
|
-
|
|
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;
|
|
674
1125
|
}
|
|
675
1126
|
|
|
676
1127
|
// lib/logger.ts
|
|
@@ -714,94 +1165,114 @@ var Logger = class {
|
|
|
714
1165
|
};
|
|
715
1166
|
|
|
716
1167
|
// lib/state.ts
|
|
717
|
-
import {
|
|
718
|
-
import {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
ensure(sessionID) {
|
|
743
|
-
const existing = this.sessions.get(sessionID);
|
|
744
|
-
if (existing) return existing;
|
|
745
|
-
const created = {
|
|
746
|
-
sessionID,
|
|
747
|
-
eligible: false,
|
|
748
|
-
lastResponseAt: 0,
|
|
749
|
-
idleSince: 0,
|
|
750
|
-
windowEndsAt: 0,
|
|
751
|
-
nextPingAt: 0,
|
|
752
|
-
pingsSent: 0,
|
|
753
|
-
busy: false,
|
|
754
|
-
warming: false,
|
|
755
|
-
active: false
|
|
756
|
-
};
|
|
757
|
-
this.sessions.set(sessionID, created);
|
|
758
|
-
return created;
|
|
759
|
-
}
|
|
760
|
-
remove(sessionID) {
|
|
761
|
-
this.sessions.delete(sessionID);
|
|
762
|
-
}
|
|
763
|
-
all() {
|
|
764
|
-
return [...this.sessions.values()];
|
|
765
|
-
}
|
|
766
|
-
/**
|
|
767
|
-
* Atomically persist a lean snapshot for the TUI. Never throws or rejects; the
|
|
768
|
-
* returned promise resolves once this snapshot and every earlier one are written.
|
|
769
|
-
*/
|
|
770
|
-
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;
|
|
771
1193
|
const snapshot = {
|
|
772
|
-
version:
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
this.all().map((s) => [
|
|
779
|
-
s.sessionID,
|
|
780
|
-
{
|
|
781
|
-
eligible: s.eligible,
|
|
782
|
-
modelLabel: s.modelLabel,
|
|
783
|
-
lastResponseAt: s.lastResponseAt,
|
|
784
|
-
idleSince: s.idleSince,
|
|
785
|
-
windowEndsAt: s.windowEndsAt,
|
|
786
|
-
nextPingAt: s.nextPingAt,
|
|
787
|
-
intervalMs: this.intervalMs,
|
|
788
|
-
pingsSent: s.pingsSent,
|
|
789
|
-
busy: s.busy,
|
|
790
|
-
warming: s.warming,
|
|
791
|
-
active: s.active,
|
|
792
|
-
lastPing: s.lastPing
|
|
793
|
-
}
|
|
794
|
-
])
|
|
795
|
-
)
|
|
1194
|
+
version: 2,
|
|
1195
|
+
pid: process.pid,
|
|
1196
|
+
instanceId,
|
|
1197
|
+
updatedAt: now(),
|
|
1198
|
+
sessions: Object.fromEntries(sessions),
|
|
1199
|
+
totals
|
|
796
1200
|
};
|
|
797
|
-
const write = () => atomicWrite(
|
|
798
|
-
|
|
799
|
-
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 {
|
|
800
1271
|
}
|
|
801
|
-
}
|
|
1272
|
+
}
|
|
802
1273
|
async function atomicWrite(path, contents) {
|
|
803
1274
|
try {
|
|
804
|
-
const tmp = `${path}.${process.pid}.${
|
|
1275
|
+
const tmp = `${path}.${process.pid}.${randomUUID2()}.tmp`;
|
|
805
1276
|
await mkdir2(dirname3(path), { recursive: true });
|
|
806
1277
|
await writeFile2(tmp, `${contents}
|
|
807
1278
|
`, "utf8");
|
|
@@ -810,54 +1281,27 @@ async function atomicWrite(path, contents) {
|
|
|
810
1281
|
}
|
|
811
1282
|
}
|
|
812
1283
|
|
|
813
|
-
// lib/system.ts
|
|
814
|
-
function keepaliveInstruction(pingToken) {
|
|
815
|
-
return [
|
|
816
|
-
"<cache_keepalive>",
|
|
817
|
-
`A user message consisting of exactly the single token "${pingToken}" is an`,
|
|
818
|
-
"automated cache-keepalive ping, not a real request. When you receive it,",
|
|
819
|
-
`reply with exactly "${pingToken}" and nothing else. Do not call any tools,`,
|
|
820
|
-
"do not explain, and do not take any action.",
|
|
821
|
-
"</cache_keepalive>"
|
|
822
|
-
].join("\n");
|
|
823
|
-
}
|
|
824
|
-
|
|
825
1284
|
// index.ts
|
|
826
1285
|
var server = async (ctx, options) => {
|
|
827
|
-
const
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
logger.
|
|
831
|
-
|
|
832
|
-
|
|
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());
|
|
833
1292
|
if (isSecureMode()) configureClientAuth(ctx.client);
|
|
834
|
-
const store =
|
|
835
|
-
const engine = new
|
|
1293
|
+
const store = createStateStore(ctx.directory, { logger: activeLogger });
|
|
1294
|
+
const engine = new ReplayWarmer(ctx.client, config, store, activeLogger, ctx.directory);
|
|
836
1295
|
engine.start();
|
|
837
|
-
const version = true ? "0.
|
|
1296
|
+
const version = true ? "0.2.0" : "dev";
|
|
838
1297
|
logger.info(`v${version} ready`);
|
|
839
|
-
const instruction = keepaliveInstruction(config.pingToken);
|
|
840
1298
|
return {
|
|
841
1299
|
event: async ({ event }) => {
|
|
842
|
-
|
|
843
|
-
},
|
|
844
|
-
// Keep the system prefix identical between real turns and pings, and make
|
|
845
|
-
// the model answer a ping with a single token.
|
|
846
|
-
"experimental.chat.system.transform": async (input, output) => {
|
|
847
|
-
if (!config.injectSystemInstruction) return;
|
|
848
|
-
const model = input.model;
|
|
849
|
-
if (!isEligibleModel(config, model?.providerID, model?.id ?? model?.modelID)) return;
|
|
850
|
-
output.system.push(instruction);
|
|
851
|
-
},
|
|
852
|
-
// Backstop: if the model ignores the instruction and tries a tool during a
|
|
853
|
-
// ping, block it — the ping turn is going to be reverted anyway.
|
|
854
|
-
"tool.execute.before": async (input) => {
|
|
855
|
-
if (engine.shouldBlockTools(input.sessionID)) {
|
|
856
|
-
throw new Error("cache-keepalive: tools are disabled during a keepalive ping");
|
|
857
|
-
}
|
|
1300
|
+
engine.onEvent(event);
|
|
858
1301
|
},
|
|
859
1302
|
dispose: async () => {
|
|
860
|
-
engine.
|
|
1303
|
+
engine.dispose();
|
|
1304
|
+
await store.dispose();
|
|
861
1305
|
}
|
|
862
1306
|
};
|
|
863
1307
|
};
|