@shawnstack/quickforge 1.3.16 → 1.3.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/dist/assets/{anthropic-CTuvxFTI.js → anthropic-Ck2DxOfr.js} +1 -1
- package/dist/assets/{azure-openai-responses-Bbdz_9hw.js → azure-openai-responses-DIoz5q4Z.js} +1 -1
- package/dist/assets/{google-Dwuw-aUO.js → google-Dau-4ve_.js} +1 -1
- package/dist/assets/{google-gemini-cli-CUAh97VN.js → google-gemini-cli-DttMmbGb.js} +1 -1
- package/dist/assets/{google-vertex-I8vqzEMR.js → google-vertex-BeukMl44.js} +1 -1
- package/dist/assets/{index-t6ITXfOr.css → index-DgJVElbv.css} +1 -1
- package/dist/assets/{index-DC0sfTYJ.js → index-Dm7aEWvT.js} +462 -444
- package/dist/assets/{mistral-BFoKj0LS.js → mistral-DxhS4Wkn.js} +1 -1
- package/dist/assets/{openai-codex-responses-BowpWoQz.js → openai-codex-responses-X3sTzNAa.js} +1 -1
- package/dist/assets/{openai-completions-DgCq7TXk.js → openai-completions-CRB9Vm0w.js} +1 -1
- package/dist/assets/{openai-responses-Bj3ccKY7.js → openai-responses-DXluu3oi.js} +1 -1
- package/dist/assets/{openai-responses-shared-B1BV7Hco.js → openai-responses-shared-f_P3e1nz.js} +1 -1
- package/dist/index.html +2 -2
- package/node_modules/@babel/runtime/package.json +1 -1
- package/node_modules/undici/lib/api/api-request.js +1 -1
- package/node_modules/undici/lib/cache/sqlite-cache-store.js +3 -3
- package/node_modules/undici/lib/core/connect.js +16 -0
- package/node_modules/undici/lib/core/request.js +17 -2
- package/node_modules/undici/lib/core/socks5-client.js +10 -5
- package/node_modules/undici/lib/core/socks5-utils.js +17 -22
- package/node_modules/undici/lib/dispatcher/client-h1.js +64 -20
- package/node_modules/undici/lib/dispatcher/client.js +6 -2
- package/node_modules/undici/lib/dispatcher/h2c-client.js +1 -1
- package/node_modules/undici/lib/mock/mock-call-history.js +15 -15
- package/node_modules/undici/lib/util/cache.js +8 -7
- package/node_modules/undici/lib/web/fetch/formdata-parser.js +17 -6
- package/node_modules/undici/lib/web/fetch/index.js +5 -2
- package/node_modules/undici/lib/web/webidl/index.js +5 -5
- package/node_modules/undici/lib/web/websocket/stream/websocketstream.js +1 -7
- package/node_modules/undici/package.json +1 -1
- package/package.json +1 -1
- package/server/agent-manager.mjs +10 -1
- package/server/auto-compaction.mjs +96 -21
- package/server/tools/definitions.mjs +2 -0
- package/server/tools/index.mjs +211 -83
|
@@ -235,9 +235,13 @@ class Client extends DispatcherBase {
|
|
|
235
235
|
...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
|
|
236
236
|
...connect
|
|
237
237
|
})
|
|
238
|
-
} else
|
|
238
|
+
} else {
|
|
239
239
|
const customConnect = connect
|
|
240
|
-
connect = (opts, callback) => customConnect({
|
|
240
|
+
connect = (opts, callback) => customConnect({
|
|
241
|
+
...opts,
|
|
242
|
+
...(socketPath != null ? { socketPath } : null),
|
|
243
|
+
...(allowH2 != null ? { allowH2 } : null)
|
|
244
|
+
}, callback)
|
|
241
245
|
}
|
|
242
246
|
|
|
243
247
|
this[kUrl] = util.parseOrigin(url)
|
|
@@ -15,7 +15,7 @@ class H2CClient extends Client {
|
|
|
15
15
|
)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const {
|
|
18
|
+
const { maxConcurrentStreams, pipelining, ...opts } =
|
|
19
19
|
clientOpts ?? {}
|
|
20
20
|
let defaultMaxConcurrentStreams = 100
|
|
21
21
|
let defaultPipelining = 100
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
const { kMockCallHistoryAddLog } = require('./mock-symbols')
|
|
4
4
|
const { InvalidArgumentError } = require('../core/errors')
|
|
5
5
|
|
|
6
|
-
function handleFilterCallsWithOptions (criteria, options, handler, store) {
|
|
6
|
+
function handleFilterCallsWithOptions (criteria, options, handler, store, allLogs) {
|
|
7
7
|
switch (options.operator) {
|
|
8
8
|
case 'OR':
|
|
9
|
-
store.push(...handler(criteria))
|
|
9
|
+
store.push(...handler(criteria, allLogs))
|
|
10
10
|
|
|
11
11
|
return store
|
|
12
12
|
case 'AND':
|
|
13
|
-
return handler
|
|
13
|
+
return handler(criteria, store)
|
|
14
14
|
default:
|
|
15
15
|
// guard -- should never happens because buildAndValidateFilterCallsOptions is called before
|
|
16
16
|
throw new InvalidArgumentError('options.operator must to be a case insensitive string equal to \'OR\' or \'AND\'')
|
|
@@ -35,14 +35,14 @@ function buildAndValidateFilterCallsOptions (options = {}) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function makeFilterCalls (parameterName) {
|
|
38
|
-
return (parameterValue) => {
|
|
38
|
+
return (parameterValue, logs) => {
|
|
39
39
|
if (typeof parameterValue === 'string' || parameterValue == null) {
|
|
40
|
-
return
|
|
40
|
+
return logs.filter((log) => {
|
|
41
41
|
return log[parameterName] === parameterValue
|
|
42
42
|
})
|
|
43
43
|
}
|
|
44
44
|
if (parameterValue instanceof RegExp) {
|
|
45
|
-
return
|
|
45
|
+
return logs.filter((log) => {
|
|
46
46
|
return parameterValue.test(log[parameterName])
|
|
47
47
|
})
|
|
48
48
|
}
|
|
@@ -175,30 +175,30 @@ class MockCallHistory {
|
|
|
175
175
|
|
|
176
176
|
const finalOptions = { operator: 'OR', ...buildAndValidateFilterCallsOptions(options) }
|
|
177
177
|
|
|
178
|
-
let maybeDuplicatedLogsFiltered = []
|
|
178
|
+
let maybeDuplicatedLogsFiltered = finalOptions.operator === 'AND' ? this.logs : []
|
|
179
179
|
if ('protocol' in criteria) {
|
|
180
|
-
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.protocol, finalOptions, this.filterCallsByProtocol, maybeDuplicatedLogsFiltered)
|
|
180
|
+
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.protocol, finalOptions, this.filterCallsByProtocol, maybeDuplicatedLogsFiltered, this.logs)
|
|
181
181
|
}
|
|
182
182
|
if ('host' in criteria) {
|
|
183
|
-
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.host, finalOptions, this.filterCallsByHost, maybeDuplicatedLogsFiltered)
|
|
183
|
+
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.host, finalOptions, this.filterCallsByHost, maybeDuplicatedLogsFiltered, this.logs)
|
|
184
184
|
}
|
|
185
185
|
if ('port' in criteria) {
|
|
186
|
-
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.port, finalOptions, this.filterCallsByPort, maybeDuplicatedLogsFiltered)
|
|
186
|
+
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.port, finalOptions, this.filterCallsByPort, maybeDuplicatedLogsFiltered, this.logs)
|
|
187
187
|
}
|
|
188
188
|
if ('origin' in criteria) {
|
|
189
|
-
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.origin, finalOptions, this.filterCallsByOrigin, maybeDuplicatedLogsFiltered)
|
|
189
|
+
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.origin, finalOptions, this.filterCallsByOrigin, maybeDuplicatedLogsFiltered, this.logs)
|
|
190
190
|
}
|
|
191
191
|
if ('path' in criteria) {
|
|
192
|
-
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.path, finalOptions, this.filterCallsByPath, maybeDuplicatedLogsFiltered)
|
|
192
|
+
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.path, finalOptions, this.filterCallsByPath, maybeDuplicatedLogsFiltered, this.logs)
|
|
193
193
|
}
|
|
194
194
|
if ('hash' in criteria) {
|
|
195
|
-
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.hash, finalOptions, this.filterCallsByHash, maybeDuplicatedLogsFiltered)
|
|
195
|
+
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.hash, finalOptions, this.filterCallsByHash, maybeDuplicatedLogsFiltered, this.logs)
|
|
196
196
|
}
|
|
197
197
|
if ('fullUrl' in criteria) {
|
|
198
|
-
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.fullUrl, finalOptions, this.filterCallsByFullUrl, maybeDuplicatedLogsFiltered)
|
|
198
|
+
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.fullUrl, finalOptions, this.filterCallsByFullUrl, maybeDuplicatedLogsFiltered, this.logs)
|
|
199
199
|
}
|
|
200
200
|
if ('method' in criteria) {
|
|
201
|
-
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.method, finalOptions, this.filterCallsByMethod, maybeDuplicatedLogsFiltered)
|
|
201
|
+
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.method, finalOptions, this.filterCallsByMethod, maybeDuplicatedLogsFiltered, this.logs)
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
const uniqLogsFiltered = [...new Set(maybeDuplicatedLogsFiltered)]
|
|
@@ -18,7 +18,7 @@ function makeCacheKey (opts) {
|
|
|
18
18
|
|
|
19
19
|
let fullPath = opts.path || '/'
|
|
20
20
|
|
|
21
|
-
if (opts.query && !pathHasQueryOrFragment(
|
|
21
|
+
if (opts.query && !pathHasQueryOrFragment(fullPath)) {
|
|
22
22
|
fullPath = serializePathWithQuery(fullPath, opts.query)
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -374,9 +374,11 @@ function assertCacheMethods (methods, name = 'CacheMethods') {
|
|
|
374
374
|
* @returns {string}
|
|
375
375
|
*/
|
|
376
376
|
function makeDeduplicationKey (cacheKey, excludeHeaders) {
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
|
|
377
|
+
// Use JSON.stringify to produce a collision-resistant key.
|
|
378
|
+
// Previous format used `:` and `=` delimiters without escaping, which
|
|
379
|
+
// allowed different header sets to produce identical keys (e.g.
|
|
380
|
+
// {a:"x:b=y"} vs {a:"x", b:"y"}). See: https://github.com/nodejs/undici/issues/5012
|
|
381
|
+
const headers = {}
|
|
380
382
|
|
|
381
383
|
if (cacheKey.headers) {
|
|
382
384
|
const sortedHeaders = Object.keys(cacheKey.headers).sort()
|
|
@@ -385,12 +387,11 @@ function makeDeduplicationKey (cacheKey, excludeHeaders) {
|
|
|
385
387
|
if (excludeHeaders?.has(header.toLowerCase())) {
|
|
386
388
|
continue
|
|
387
389
|
}
|
|
388
|
-
|
|
389
|
-
key += `:${header}=${Array.isArray(value) ? value.join(',') : value}`
|
|
390
|
+
headers[header] = cacheKey.headers[header]
|
|
390
391
|
}
|
|
391
392
|
}
|
|
392
393
|
|
|
393
|
-
return
|
|
394
|
+
return JSON.stringify([cacheKey.origin, cacheKey.method, cacheKey.path, headers])
|
|
394
395
|
}
|
|
395
396
|
|
|
396
397
|
module.exports = {
|
|
@@ -204,7 +204,7 @@ function multipartFormDataParser (input, mimeType) {
|
|
|
204
204
|
* Parses content-disposition attributes (e.g., name="value" or filename*=utf-8''encoded)
|
|
205
205
|
* @param {Buffer} input
|
|
206
206
|
* @param {{ position: number }} position
|
|
207
|
-
* @returns {{ name: string, value: string }}
|
|
207
|
+
* @returns {{ name: string, value: string, extended: boolean } | null}
|
|
208
208
|
*/
|
|
209
209
|
function parseContentDispositionAttribute (input, position) {
|
|
210
210
|
// Skip leading semicolon and whitespace
|
|
@@ -304,7 +304,7 @@ function parseContentDispositionAttribute (input, position) {
|
|
|
304
304
|
value = decoder.decode(tokenValue)
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
return { name: attrNameStr, value }
|
|
307
|
+
return { name: attrNameStr, value, extended: isExtended }
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
/**
|
|
@@ -368,6 +368,9 @@ function parseMultipartFormDataHeaders (input, position) {
|
|
|
368
368
|
switch (bufferToLowerCasedHeaderName(headerName)) {
|
|
369
369
|
case 'content-disposition': {
|
|
370
370
|
name = filename = null
|
|
371
|
+
// Track whether filename was set from the extended (RFC 5987) form so
|
|
372
|
+
// a subsequent legacy `filename` attribute does not override it.
|
|
373
|
+
let filenameIsExtended = false
|
|
371
374
|
|
|
372
375
|
// Collect the disposition type (should be "form-data")
|
|
373
376
|
const dispositionType = collectASequenceOfBytes(
|
|
@@ -383,8 +386,8 @@ function parseMultipartFormDataHeaders (input, position) {
|
|
|
383
386
|
// Parse attributes recursively until CRLF
|
|
384
387
|
while (
|
|
385
388
|
position.position < input.length &&
|
|
386
|
-
input[position.position] !== 0x0d
|
|
387
|
-
input[position.position + 1] !== 0x0a
|
|
389
|
+
(input[position.position] !== 0x0d ||
|
|
390
|
+
input[position.position + 1] !== 0x0a)
|
|
388
391
|
) {
|
|
389
392
|
const attribute = parseContentDispositionAttribute(input, position)
|
|
390
393
|
|
|
@@ -395,7 +398,15 @@ function parseMultipartFormDataHeaders (input, position) {
|
|
|
395
398
|
if (attribute.name === 'name') {
|
|
396
399
|
name = attribute.value
|
|
397
400
|
} else if (attribute.name === 'filename') {
|
|
398
|
-
|
|
401
|
+
// Per RFC 5987 §4.1, when both legacy and extended forms of the
|
|
402
|
+
// same parameter are present, the extended (filename*) form takes
|
|
403
|
+
// precedence regardless of the order they appear in.
|
|
404
|
+
if (attribute.extended) {
|
|
405
|
+
filename = attribute.value
|
|
406
|
+
filenameIsExtended = true
|
|
407
|
+
} else if (!filenameIsExtended) {
|
|
408
|
+
filename = attribute.value
|
|
409
|
+
}
|
|
399
410
|
}
|
|
400
411
|
}
|
|
401
412
|
|
|
@@ -448,7 +459,7 @@ function parseMultipartFormDataHeaders (input, position) {
|
|
|
448
459
|
|
|
449
460
|
// 2.9. If position does not point to a sequence of bytes starting with 0x0D 0x0A
|
|
450
461
|
// (CR LF), return failure. Otherwise, advance position by 2 (past the newline).
|
|
451
|
-
if (input[position.position] !== 0x0d
|
|
462
|
+
if (input[position.position] !== 0x0d || input[position.position + 1] !== 0x0a) {
|
|
452
463
|
throw parsingError('expected CRLF')
|
|
453
464
|
} else {
|
|
454
465
|
position.position += 2
|
|
@@ -1030,7 +1030,7 @@ function fetchFinale (fetchParams, response) {
|
|
|
1030
1030
|
let responseStatus = 0
|
|
1031
1031
|
|
|
1032
1032
|
// 7. If fetchParams’s request’s mode is not "navigate" or response’s has-cross-origin-redirects is false:
|
|
1033
|
-
if (fetchParams.request.mode !== '
|
|
1033
|
+
if (fetchParams.request.mode !== 'navigate' || !response.hasCrossOriginRedirects) {
|
|
1034
1034
|
// 1. Set responseStatus to response’s status.
|
|
1035
1035
|
responseStatus = response.status
|
|
1036
1036
|
|
|
@@ -1433,7 +1433,10 @@ async function httpNetworkOrCacheFetch (
|
|
|
1433
1433
|
// 8. If contentLengthHeaderValue is non-null, then append
|
|
1434
1434
|
// `Content-Length`/contentLengthHeaderValue to httpRequest’s header
|
|
1435
1435
|
// list.
|
|
1436
|
-
if (
|
|
1436
|
+
if (
|
|
1437
|
+
contentLengthHeaderValue != null &&
|
|
1438
|
+
!httpRequest.headersList.contains('content-length', true)
|
|
1439
|
+
) {
|
|
1437
1440
|
httpRequest.headersList.append('content-length', contentLengthHeaderValue, true)
|
|
1438
1441
|
}
|
|
1439
1442
|
|
|
@@ -190,10 +190,10 @@ webidl.util.ConvertToInt = function (V, bitLength, signedness, flags) {
|
|
|
190
190
|
} else {
|
|
191
191
|
// 3. Otherwise:
|
|
192
192
|
|
|
193
|
-
// 1. Let lowerBound be -2^bitLength − 1.
|
|
194
|
-
lowerBound = Math.pow(
|
|
193
|
+
// 1. Let lowerBound be -2^(bitLength − 1).
|
|
194
|
+
lowerBound = -Math.pow(2, bitLength - 1)
|
|
195
195
|
|
|
196
|
-
// 2. Let upperBound be 2^bitLength − 1 − 1.
|
|
196
|
+
// 2. Let upperBound be 2^(bitLength − 1) − 1.
|
|
197
197
|
upperBound = Math.pow(2, bitLength - 1) - 1
|
|
198
198
|
}
|
|
199
199
|
|
|
@@ -272,9 +272,9 @@ webidl.util.ConvertToInt = function (V, bitLength, signedness, flags) {
|
|
|
272
272
|
// 10. Set x to x modulo 2^bitLength.
|
|
273
273
|
x = x % Math.pow(2, bitLength)
|
|
274
274
|
|
|
275
|
-
// 11. If signedness is "signed" and x ≥ 2^bitLength − 1,
|
|
275
|
+
// 11. If signedness is "signed" and x ≥ 2^(bitLength − 1),
|
|
276
276
|
// then return x − 2^bitLength.
|
|
277
|
-
if (signedness === 'signed' && x >= Math.pow(2, bitLength
|
|
277
|
+
if (signedness === 'signed' && x >= Math.pow(2, bitLength - 1)) {
|
|
278
278
|
return x - Math.pow(2, bitLength)
|
|
279
279
|
}
|
|
280
280
|
|
|
@@ -284,12 +284,6 @@ class WebSocketStream {
|
|
|
284
284
|
start: (controller) => {
|
|
285
285
|
this.#readableStreamController = controller
|
|
286
286
|
},
|
|
287
|
-
pull (controller) {
|
|
288
|
-
let chunk
|
|
289
|
-
while (controller.desiredSize > 0 && (chunk = response.socket.read()) !== null) {
|
|
290
|
-
controller.enqueue(chunk)
|
|
291
|
-
}
|
|
292
|
-
},
|
|
293
287
|
cancel: (reason) => this.#cancel(reason)
|
|
294
288
|
})
|
|
295
289
|
|
|
@@ -338,7 +332,7 @@ class WebSocketStream {
|
|
|
338
332
|
try {
|
|
339
333
|
chunk = utf8Decode(data)
|
|
340
334
|
} catch {
|
|
341
|
-
failWebsocketConnection(this.#handler, 'Received invalid UTF-8 in text frame.')
|
|
335
|
+
failWebsocketConnection(this.#handler, 1007, 'Received invalid UTF-8 in text frame.')
|
|
342
336
|
return
|
|
343
337
|
}
|
|
344
338
|
} else if (type === opcodes.BINARY) {
|
package/package.json
CHANGED
package/server/agent-manager.mjs
CHANGED
|
@@ -360,6 +360,7 @@ function updateSessionMessages(session, messages) {
|
|
|
360
360
|
function resetSessionCompaction(session) {
|
|
361
361
|
session.contextCompaction = null
|
|
362
362
|
session.lastAutoCompactAt = null
|
|
363
|
+
session.lastAutoCompactRejected = null
|
|
363
364
|
session.lastTransformedContextMessages = null
|
|
364
365
|
session.autoCompacting = false
|
|
365
366
|
}
|
|
@@ -642,7 +643,7 @@ function compactedContextMessages(messages) {
|
|
|
642
643
|
}
|
|
643
644
|
|
|
644
645
|
async function transformSessionContext(session, messages, signal) {
|
|
645
|
-
await maybeAutoCompactSession({
|
|
646
|
+
const autoCompactResult = await maybeAutoCompactSession({
|
|
646
647
|
session,
|
|
647
648
|
messages,
|
|
648
649
|
signal,
|
|
@@ -651,6 +652,13 @@ async function transformSessionContext(session, messages, signal) {
|
|
|
651
652
|
logger,
|
|
652
653
|
confirmAutoCompact: createAutoCompactApprovalPromise,
|
|
653
654
|
})
|
|
655
|
+
if (!autoCompactResult.compacted && autoCompactResult.usage && autoCompactResult.reason && autoCompactResult.reason !== 'below_threshold') {
|
|
656
|
+
logger.info(`Auto compact skipped for session ${session.sessionId}: ${autoCompactResult.reason}`, {
|
|
657
|
+
sessionId: session.sessionId,
|
|
658
|
+
reason: autoCompactResult.reason,
|
|
659
|
+
usage: autoCompactResult.usage,
|
|
660
|
+
})
|
|
661
|
+
}
|
|
654
662
|
const transformedMessages = buildAutoCompactLoopMessages(session, messages)
|
|
655
663
|
session.lastTransformedContextMessages = transformedMessages
|
|
656
664
|
return applyActiveCommandPrompt(compactedContextMessages(transformedMessages), session?.activeCommandPrompt)
|
|
@@ -837,6 +845,7 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
837
845
|
lastTransformedContextMessages: null,
|
|
838
846
|
autoCompacting: false,
|
|
839
847
|
lastAutoCompactAt: null,
|
|
848
|
+
lastAutoCompactRejected: null,
|
|
840
849
|
/** Track active SSE connections. Only one SSE stream allowed per session to prevent
|
|
841
850
|
* connection-pool exhaustion when two browser tabs load the same session. */
|
|
842
851
|
sseConnected: false,
|
|
@@ -12,6 +12,7 @@ export const DEFAULT_AUTO_COMPACT_SETTINGS = {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const AUTO_COMPACT_MIN_INTERVAL_MS = 30_000
|
|
15
|
+
const AUTO_COMPACT_REJECTION_SUPPRESS_MS = 10 * 60_000
|
|
15
16
|
|
|
16
17
|
function clampNumber(value, fallback, min, max) {
|
|
17
18
|
const parsed = Number(value)
|
|
@@ -85,16 +86,48 @@ function estimateMessagesChars(messages) {
|
|
|
85
86
|
}, 0)
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
function messageTimestampMs(message) {
|
|
90
|
+
const timestamp = message?.timestamp
|
|
91
|
+
if (typeof timestamp === 'number') return timestamp
|
|
92
|
+
if (typeof timestamp === 'string') {
|
|
93
|
+
const parsed = Date.parse(timestamp)
|
|
94
|
+
return Number.isNaN(parsed) ? 0 : parsed
|
|
95
|
+
}
|
|
96
|
+
return 0
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function latestCompactTimestampMs(session) {
|
|
100
|
+
return messageTimestampMs(session?.contextCompaction?.summaryMessage)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function latestKnownInputTokens(messages, sinceTimestamp = 0) {
|
|
104
|
+
let latestTimestamp = -1
|
|
105
|
+
let latestInput = 0
|
|
106
|
+
for (const message of Array.isArray(messages) ? messages : []) {
|
|
107
|
+
if (message?.role !== 'assistant' || !message.usage) continue
|
|
108
|
+
const timestamp = messageTimestampMs(message)
|
|
109
|
+
if (sinceTimestamp > 0 && timestamp <= sinceTimestamp) continue
|
|
110
|
+
if (timestamp < latestTimestamp) continue
|
|
111
|
+
const input = Math.max(0, Number(message.usage.input ?? message.usage.totalTokens) || 0)
|
|
112
|
+
if (input <= 0) continue
|
|
113
|
+
latestTimestamp = timestamp
|
|
114
|
+
latestInput = input
|
|
115
|
+
}
|
|
116
|
+
return latestInput
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function estimateContextUsage({ systemPrompt, messages, tools, model, knownInputTokens = 0 }) {
|
|
89
120
|
const contextWindow = Number(model?.contextWindow) || 0
|
|
90
121
|
const reservedOutputTokens = Math.max(0, Number(model?.maxTokens) || 4096)
|
|
91
|
-
const
|
|
122
|
+
const estimatedInputTokens =
|
|
92
123
|
estimateTextTokens(systemPrompt) +
|
|
93
124
|
estimateMessagesTokens(messages) +
|
|
94
125
|
estimateTextTokens(safeJson(tools))
|
|
126
|
+
const knownInput = Math.max(0, Number(knownInputTokens) || 0)
|
|
127
|
+
const inputTokens = Math.max(estimatedInputTokens, knownInput)
|
|
95
128
|
const totalTokens = inputTokens + reservedOutputTokens
|
|
96
129
|
const percent = contextWindow > 0 ? Math.round((totalTokens / contextWindow) * 1000) / 10 : 0
|
|
97
|
-
return { inputTokens, reservedOutputTokens, totalTokens, contextWindow, percent }
|
|
130
|
+
return { inputTokens, estimatedInputTokens, knownInputTokens: knownInput, reservedOutputTokens, totalTokens, contextWindow, percent }
|
|
98
131
|
}
|
|
99
132
|
|
|
100
133
|
function isUserMessage(message) {
|
|
@@ -112,6 +145,34 @@ function tailStartForRecentTurns(messages, keepRecentTurns) {
|
|
|
112
145
|
return 0
|
|
113
146
|
}
|
|
114
147
|
|
|
148
|
+
function shouldSuppressAfterRejection(session, messages, usage) {
|
|
149
|
+
const rejection = session?.lastAutoCompactRejected
|
|
150
|
+
if (!rejection) return false
|
|
151
|
+
const now = Date.now()
|
|
152
|
+
const rejectedAt = Number(rejection.rejectedAt) || 0
|
|
153
|
+
if (rejectedAt <= 0 || now - rejectedAt > AUTO_COMPACT_REJECTION_SUPPRESS_MS) return false
|
|
154
|
+
|
|
155
|
+
const rejectedMessageCount = Number(rejection.messageCount) || 0
|
|
156
|
+
const currentMessageCount = Array.isArray(messages) ? messages.length : 0
|
|
157
|
+
if (currentMessageCount >= rejectedMessageCount + 3) return false
|
|
158
|
+
|
|
159
|
+
const rejectedPercent = Number(rejection.percent) || 0
|
|
160
|
+
const currentPercent = Number(usage?.percent) || 0
|
|
161
|
+
return currentPercent <= rejectedPercent + 5
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function markAutoCompactRejected(session, messages, usage) {
|
|
165
|
+
session.lastAutoCompactRejected = {
|
|
166
|
+
rejectedAt: Date.now(),
|
|
167
|
+
messageCount: Array.isArray(messages) ? messages.length : 0,
|
|
168
|
+
percent: Number(usage?.percent) || 0,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function clearAutoCompactRejected(session) {
|
|
173
|
+
session.lastAutoCompactRejected = null
|
|
174
|
+
}
|
|
175
|
+
|
|
115
176
|
function compactSummaryText(message) {
|
|
116
177
|
const content = message?.content
|
|
117
178
|
const text = typeof content === 'string'
|
|
@@ -150,37 +211,29 @@ function buildCompactionSourceMessages(session, messages, tailStart) {
|
|
|
150
211
|
export function buildAutoCompactLoopMessages(session, messages) {
|
|
151
212
|
const summaryMessage = session?.contextCompaction?.summaryMessage
|
|
152
213
|
if (!summaryMessage) return messages
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
return [summaryMessage, ...
|
|
214
|
+
const source = Array.isArray(messages) ? messages : []
|
|
215
|
+
const compactedUpToIndex = Math.min(source.length, Math.max(0, Number(session.contextCompaction?.compactedUpToIndex) || 0))
|
|
216
|
+
return [summaryMessage, ...source.slice(compactedUpToIndex)]
|
|
156
217
|
}
|
|
157
218
|
|
|
158
219
|
export async function maybeAutoCompactSession({ session, messages, signal, emitSessionEvent, persistSession, logger, confirmAutoCompact }) {
|
|
159
220
|
if (!session || session.autoCompacting) return { compacted: false }
|
|
160
221
|
const settings = await readAutoCompactSettings()
|
|
161
|
-
if (!settings.enabled) return { compacted: false }
|
|
162
|
-
if (signal?.aborted) return { compacted: false }
|
|
222
|
+
if (!settings.enabled) return { compacted: false, reason: 'disabled' }
|
|
223
|
+
if (signal?.aborted) return { compacted: false, reason: 'aborted' }
|
|
163
224
|
|
|
164
225
|
const loopMessages = buildAutoCompactLoopMessages(session, messages)
|
|
226
|
+
const knownInputTokens = latestKnownInputTokens(messages, latestCompactTimestampMs(session))
|
|
165
227
|
const usage = estimateContextUsage({
|
|
166
228
|
systemPrompt: session.agent.state.systemPrompt,
|
|
167
229
|
messages: loopMessages,
|
|
168
230
|
tools: session.agent.state.tools,
|
|
169
231
|
model: session.model,
|
|
232
|
+
knownInputTokens,
|
|
170
233
|
})
|
|
171
|
-
if (!usage.contextWindow
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
type: 'auto_compact_threshold_reached',
|
|
175
|
-
usage,
|
|
176
|
-
thresholdPercent: settings.thresholdPercent,
|
|
177
|
-
requireConfirmation: settings.requireConfirmation,
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
if (settings.requireConfirmation) {
|
|
181
|
-
const approved = await confirmAutoCompact?.(session, { usage, settings })
|
|
182
|
-
if (!approved || signal?.aborted) return { compacted: false, usage, reason: approved === false ? 'user_rejected' : 'confirmation_unavailable' }
|
|
183
|
-
}
|
|
234
|
+
if (!usage.contextWindow) return { compacted: false, usage, reason: 'missing_context_window' }
|
|
235
|
+
if (usage.percent < settings.thresholdPercent) return { compacted: false, usage, reason: 'below_threshold' }
|
|
236
|
+
if (shouldSuppressAfterRejection(session, messages, usage)) return { compacted: false, usage, reason: 'user_rejected_recently' }
|
|
184
237
|
|
|
185
238
|
const now = Date.now()
|
|
186
239
|
if (session.lastAutoCompactAt && now - session.lastAutoCompactAt < AUTO_COMPACT_MIN_INTERVAL_MS) {
|
|
@@ -196,6 +249,21 @@ export async function maybeAutoCompactSession({ session, messages, signal, emitS
|
|
|
196
249
|
return { compacted: false, usage, reason: 'not_enough_history' }
|
|
197
250
|
}
|
|
198
251
|
|
|
252
|
+
emitSessionEvent?.(session, {
|
|
253
|
+
type: 'auto_compact_threshold_reached',
|
|
254
|
+
usage,
|
|
255
|
+
thresholdPercent: settings.thresholdPercent,
|
|
256
|
+
requireConfirmation: settings.requireConfirmation,
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
if (settings.requireConfirmation) {
|
|
260
|
+
const approved = await confirmAutoCompact?.(session, { usage, settings })
|
|
261
|
+
if (!approved || signal?.aborted) {
|
|
262
|
+
if (approved === false) markAutoCompactRejected(session, messages, usage)
|
|
263
|
+
return { compacted: false, usage, reason: approved === false ? 'user_rejected' : 'confirmation_unavailable' }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
199
267
|
session.autoCompacting = true
|
|
200
268
|
try {
|
|
201
269
|
const result = await compactConversation({
|
|
@@ -225,6 +293,7 @@ export async function maybeAutoCompactSession({ session, messages, signal, emitS
|
|
|
225
293
|
usageBefore: usage,
|
|
226
294
|
thresholdPercent: settings.thresholdPercent,
|
|
227
295
|
}
|
|
296
|
+
clearAutoCompactRejected(session)
|
|
228
297
|
session.lastAutoCompactAt = now
|
|
229
298
|
await persistSession(session)
|
|
230
299
|
emitSessionEvent(session, {
|
|
@@ -242,6 +311,12 @@ export async function maybeAutoCompactSession({ session, messages, signal, emitS
|
|
|
242
311
|
return { compacted: true, usage }
|
|
243
312
|
} catch (error) {
|
|
244
313
|
logger?.warn?.(`Auto compact failed for session ${session.sessionId}:`, error?.message || error, { sessionId: session.sessionId })
|
|
314
|
+
emitSessionEvent?.(session, {
|
|
315
|
+
type: 'auto_compact_failed',
|
|
316
|
+
usage,
|
|
317
|
+
thresholdPercent: settings.thresholdPercent,
|
|
318
|
+
error: error?.message || String(error),
|
|
319
|
+
})
|
|
245
320
|
return { compacted: false, usage, reason: 'error', error }
|
|
246
321
|
} finally {
|
|
247
322
|
session.autoCompacting = false
|
|
@@ -68,6 +68,8 @@ export const workspaceTools = [
|
|
|
68
68
|
description: 'Run a shell command in the project bound to this chat. Use this for lint, build, tests, git status, and diagnostics.',
|
|
69
69
|
parameters: Type.Object({
|
|
70
70
|
command: Type.String({ description: 'Command to execute in the workspace.' }),
|
|
71
|
+
timeoutMs: Type.Optional(Type.Number({ description: 'Command timeout in milliseconds. Defaults to 30 minutes and is clamped to the supported range.', default: 1800000 })),
|
|
72
|
+
description: Type.Optional(Type.String({ description: 'Short explanation of why this command is being run.' })),
|
|
71
73
|
}),
|
|
72
74
|
executionMode: 'sequential',
|
|
73
75
|
},
|