pi-ui-extend 0.1.44 → 0.1.45
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.
|
@@ -383,11 +383,86 @@ export function resolveIdToBoundary(
|
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
const meta = state.messageMetaSnapshot.get(id)
|
|
386
|
-
if (meta) return
|
|
386
|
+
if (meta) return resolveMetaBoundary(meta, field, state)
|
|
387
387
|
|
|
388
388
|
const ts = state.messageIdSnapshot.get(id)
|
|
389
|
-
if (ts
|
|
390
|
-
|
|
389
|
+
if (ts !== undefined) return { timestamp: ts }
|
|
390
|
+
|
|
391
|
+
// ── Stale mNNN fallback: direction-only clamp ────────────────────────
|
|
392
|
+
// When compression or pruning removes messages between context passes,
|
|
393
|
+
// positional mNNN IDs shift (e.g. an end boundary m145 becomes m123
|
|
394
|
+
// after 22 messages are removed). Clamp to the closest valid ID, but
|
|
395
|
+
// ONLY in a direction that preserves the range's semantics:
|
|
396
|
+
// - start boundary: clamp upward to the first available ID at or after
|
|
397
|
+
// the requested number. The start must never move backwards into
|
|
398
|
+
// older content; if no such ID exists, the requested start is gone
|
|
399
|
+
// and we cannot safely compress — throw.
|
|
400
|
+
// - end boundary: clamp downward to the last available ID at or before
|
|
401
|
+
// the requested number. The end must never move forwards into newer
|
|
402
|
+
// content; if no such ID exists, throw.
|
|
403
|
+
// The previous implementation fell back to the highest available ID in
|
|
404
|
+
// both "no match" cases, which could clamp e.g. m010..m010 over a
|
|
405
|
+
// snapshot of m001..m003 to a single-message block over m003 — silently
|
|
406
|
+
// compressing the wrong content.
|
|
407
|
+
const mMatch = id.match(/^m(\d+)$/i)
|
|
408
|
+
if (mMatch && state.messageIdSnapshot.size > 0) {
|
|
409
|
+
const requestedNum = parseInt(mMatch[1]!, 10)
|
|
410
|
+
const allIds = sortIds([...state.messageIdSnapshot.keys()])
|
|
411
|
+
const allNums = allIds
|
|
412
|
+
.map((mid) => {
|
|
413
|
+
const n = mid.match(/^m(\d+)$/i)
|
|
414
|
+
return n ? { id: mid, num: parseInt(n[1]!, 10) } : null
|
|
415
|
+
})
|
|
416
|
+
.filter((entry): entry is { id: string; num: number } => entry !== null)
|
|
417
|
+
.sort((a, b) => a.num - b.num)
|
|
418
|
+
|
|
419
|
+
if (allNums.length > 0) {
|
|
420
|
+
let clamped: { id: string; num: number } | undefined
|
|
421
|
+
if (field === "startTimestamp") {
|
|
422
|
+
clamped = allNums.find((entry) => entry.num >= requestedNum)
|
|
423
|
+
} else {
|
|
424
|
+
for (let i = allNums.length - 1; i >= 0; i--) {
|
|
425
|
+
if (allNums[i]!.num <= requestedNum) {
|
|
426
|
+
clamped = allNums[i]
|
|
427
|
+
break
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (clamped) {
|
|
432
|
+
const clampedMeta = state.messageMetaSnapshot.get(clamped.id)
|
|
433
|
+
if (clampedMeta) return resolveMetaBoundary(clampedMeta, field, state)
|
|
434
|
+
const clampedTs = state.messageIdSnapshot.get(clamped.id)
|
|
435
|
+
if (clampedTs !== undefined) return { timestamp: clampedTs }
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
throw unknownCompressionIdError(id, state)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Resolve a `MessageIdMeta` to a compression boundary. When the meta is a
|
|
445
|
+
* synthetic placeholder for an active compression block (`meta.blockId` is
|
|
446
|
+
* set), resolve to that block's stored boundary so the caller rolls the
|
|
447
|
+
* block up instead of nesting a new block on top of the placeholder. This
|
|
448
|
+
* matters for both exact-match and clamped mNNN IDs: a model-visible mNNN
|
|
449
|
+
* may itself represent a previously compressed section.
|
|
450
|
+
*/
|
|
451
|
+
function resolveMetaBoundary(
|
|
452
|
+
meta: MessageIdMeta,
|
|
453
|
+
field: "startTimestamp" | "endTimestamp",
|
|
454
|
+
state: DcpState,
|
|
455
|
+
): ResolvedCompressionBoundary {
|
|
456
|
+
if (meta.blockId !== undefined) {
|
|
457
|
+
const block = state.compressionBlocks.find((b) => b.id === meta.blockId && b.active)
|
|
458
|
+
if (block) {
|
|
459
|
+
return {
|
|
460
|
+
timestamp: block[field],
|
|
461
|
+
stableId: field === "startTimestamp" ? block.startMessageId : block.endMessageId,
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return { timestamp: meta.timestamp, stableId: meta.stableId }
|
|
391
466
|
}
|
|
392
467
|
|
|
393
468
|
/**
|
|
@@ -13,60 +13,76 @@ function findBoundaryIndex(messages: any[], stableId: string | undefined, timest
|
|
|
13
13
|
return messages.findIndex((m, index) => messageMatchesBoundary(m, index, stableId, timestamp));
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function collectToolCallIds(messages: any[]): Set<string> {
|
|
17
|
-
const ids = new Set<string>();
|
|
18
|
-
for (const msg of messages) {
|
|
19
|
-
if (typeof msg?.toolCallId === "string") ids.add(msg.toolCallId);
|
|
20
|
-
if (msg?.role !== "assistant" || !Array.isArray(msg.content)) continue;
|
|
21
|
-
for (const block of msg.content) {
|
|
22
|
-
if (block?.type === "toolCall" && typeof block.id === "string") ids.add(block.id);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return ids;
|
|
26
|
-
}
|
|
27
16
|
|
|
28
17
|
export function syncCompressionBlocks(messages: any[], state: DcpState, config: DcpConfig): void {
|
|
29
18
|
if (state.compressionBlocks.length === 0) return;
|
|
30
19
|
|
|
31
|
-
|
|
20
|
+
// Determine the conversation's timestamp range so we can tell whether a
|
|
21
|
+
// block's range is genuinely outside the current session history (which
|
|
22
|
+
// warrants deactivation) vs merely having its boundary messages pruned
|
|
23
|
+
// (which is normal and should NOT cause deactivation).
|
|
24
|
+
let conversationMinTs = Infinity;
|
|
25
|
+
let conversationMaxTs = -Infinity;
|
|
26
|
+
for (const msg of messages) {
|
|
27
|
+
const ts = msg?.timestamp;
|
|
28
|
+
if (typeof ts === "number" && Number.isFinite(ts)) {
|
|
29
|
+
if (ts < conversationMinTs) conversationMinTs = ts;
|
|
30
|
+
if (ts > conversationMaxTs) conversationMaxTs = ts;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
32
33
|
|
|
33
34
|
for (const block of state.compressionBlocks) {
|
|
34
35
|
if (!block.active || block.deactivatedByUser) continue;
|
|
35
36
|
|
|
37
|
+
// ── Skip the missing-origin-compress-call check ────────────────────
|
|
38
|
+
// The compress tool-call that *created* this block is not the block's
|
|
39
|
+
// content — once the block exists the tool-call is irrelevant and will
|
|
40
|
+
// naturally be pruned by tool-output pruning, session compaction, or
|
|
41
|
+
// nested compression. Deactivating a valid block because its creation
|
|
42
|
+
// tool-call was pruned silently loses the compressed summary and forces
|
|
43
|
+
// the original (larger) messages to be re-sent, which also invalidates
|
|
44
|
+
// the provider's prompt prefix cache.
|
|
45
|
+
|
|
46
|
+
// ── Boundary validation ────────────────────────────────────────────
|
|
47
|
+
// Only deactivate when the block's timestamp range falls entirely
|
|
48
|
+
// outside the conversation's time range, meaning the session genuinely
|
|
49
|
+
// does not contain that history (e.g. after a branch switch to a
|
|
50
|
+
// completely different conversation). When boundary messages are merely
|
|
51
|
+
// absent because they were themselves pruned, compressed, or have
|
|
52
|
+
// fragile timestamp-based stable IDs, the block is still valid —
|
|
53
|
+
// applyCompressionBlocks already handles missing boundaries gracefully
|
|
54
|
+
// by skipping splicing when findBoundaryIndex returns -1.
|
|
36
55
|
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
56
|
+
Number.isFinite(block.startTimestamp) &&
|
|
57
|
+
Number.isFinite(block.endTimestamp) &&
|
|
58
|
+
Number.isFinite(conversationMinTs) &&
|
|
59
|
+
Number.isFinite(conversationMaxTs)
|
|
40
60
|
) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
reason: "missing-origin-compress-call",
|
|
46
|
-
topic: block.topic,
|
|
47
|
-
createdByToolCallId: block.createdByToolCallId,
|
|
48
|
-
activeBlocksAfter: state.compressionBlocks.filter((b) => b.active).length,
|
|
49
|
-
});
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
61
|
+
// Block range is entirely before or entirely after the conversation
|
|
62
|
+
const blockEntirelyOutside =
|
|
63
|
+
block.endTimestamp < conversationMinTs ||
|
|
64
|
+
block.startTimestamp > conversationMaxTs;
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
if (blockEntirelyOutside) {
|
|
67
|
+
// Before deactivating, check if boundary messages can still be
|
|
68
|
+
// found by their stable ID — timestamps can change across
|
|
69
|
+
// session reloads while the underlying message persists.
|
|
70
|
+
const startFound = findBoundaryIndex(messages, block.startMessageId, block.startTimestamp) !== -1;
|
|
71
|
+
const endFound = findBoundaryIndex(messages, block.endMessageId, block.endTimestamp) !== -1;
|
|
55
72
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
});
|
|
73
|
+
if (!startFound && !endFound) {
|
|
74
|
+
block.active = false;
|
|
75
|
+
block.deactivatedReason = "outside-conversation-range";
|
|
76
|
+
writeDcpDebugLog(config, "block.auto_deactivated", {
|
|
77
|
+
blockId: `b${block.id}`,
|
|
78
|
+
reason: "outside-conversation-range",
|
|
79
|
+
topic: block.topic,
|
|
80
|
+
blockRange: [block.startTimestamp, block.endTimestamp],
|
|
81
|
+
conversationRange: [conversationMinTs, conversationMaxTs],
|
|
82
|
+
activeBlocksAfter: state.compressionBlocks.filter((b) => b.active).length,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
70
86
|
}
|
|
71
87
|
}
|
|
72
88
|
}
|