praxis-agent 0.25.0 → 0.26.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.
|
@@ -244,6 +244,13 @@ export declare class ClaudeSessionService {
|
|
|
244
244
|
private sessionNameEntries;
|
|
245
245
|
private hasSessionName;
|
|
246
246
|
private sessionName;
|
|
247
|
+
/** Conservative selective-preservation seam for manual compact: when the
|
|
248
|
+
* durable session memory watermark matches an active entry, summarize the
|
|
249
|
+
* last good memory artifact plus the post-watermark branch and retain a
|
|
250
|
+
* recent suffix. Returns null to fall back to the existing full-compaction
|
|
251
|
+
* behavior (missing/invalid watermark, empty projection, or oversized
|
|
252
|
+
* projection). */
|
|
253
|
+
private selectMemoryPreservedCompact;
|
|
247
254
|
private lastMessageUuid;
|
|
248
255
|
private promptIdForToolCall;
|
|
249
256
|
private assistantIdForToolCall;
|
|
@@ -13,7 +13,7 @@ import { ClaudeFileHistory } from '../compatibility/claude/file-history.js';
|
|
|
13
13
|
import { getClaudePrLink } from '../compatibility/claude/pr-links.js';
|
|
14
14
|
import { getClaudeAgentSetting, getClaudeLastPrompt, projectClaudeDisplayTranscript, projectClaudeModelMessages, } from '../compatibility/claude/projection.js';
|
|
15
15
|
import { selectClaudeSchemaAdapter, } from '../compatibility/claude/schema.js';
|
|
16
|
-
import { findUnresolvedClaudeToolCalls } from '../compatibility/claude/tool-links.js';
|
|
16
|
+
import { findUnresolvedClaudeToolCalls, getClaudeContentBlocks, } from '../compatibility/claude/tool-links.js';
|
|
17
17
|
import { createClaudeAgentSettingEntry, createClaudeHookAttachmentEntries, createClaudeLastPromptEntry, createClaudeRuleAttachmentEntry, translateProviderEvents, } from '../compatibility/claude/translation.js';
|
|
18
18
|
import { AgentRunCancelledError, AgentRuntime, } from '../core/runtime.js';
|
|
19
19
|
import { BackgroundTaskRuntime, } from './background-task-runtime.js';
|
|
@@ -302,6 +302,114 @@ Update the durable session memory from the conversation so far. Preserve:
|
|
|
302
302
|
Omit transient chatter, credentials, secrets, or personal data.
|
|
303
303
|
|
|
304
304
|
Return ONLY an updated Markdown document that becomes the session's durable memory. Do not call tools. Do not include any prose outside the Markdown document.`;
|
|
305
|
+
/** Memory-anchored manual compact keeps a recent suffix of at least five
|
|
306
|
+
* text-bearing messages and approximately ten thousand tokens when the active
|
|
307
|
+
* branch can provide them. Larger suffixes stay conservative. */
|
|
308
|
+
const MEMORY_COMPACT_MIN_SUFFIX_MESSAGES = 5;
|
|
309
|
+
const MEMORY_COMPACT_MIN_SUFFIX_TOKENS = 10_000;
|
|
310
|
+
/** Above this estimated compactor projection the selective path falls back to
|
|
311
|
+
* the existing full-compaction behavior instead of building an oversized
|
|
312
|
+
* projection. */
|
|
313
|
+
const MEMORY_COMPACT_MAX_PROJECTION_TOKENS = 40_000;
|
|
314
|
+
function isTextBearingClaudeEntry(entry) {
|
|
315
|
+
if (typeof entry.message !== 'object' ||
|
|
316
|
+
entry.message === null ||
|
|
317
|
+
Array.isArray(entry.message)) {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
const message = entry.message;
|
|
321
|
+
const content = message.content;
|
|
322
|
+
if (message.role === 'user') {
|
|
323
|
+
if (typeof content === 'string')
|
|
324
|
+
return content.trim().length > 0;
|
|
325
|
+
if (!Array.isArray(content))
|
|
326
|
+
return false;
|
|
327
|
+
return content.some((block) => typeof block === 'object' &&
|
|
328
|
+
block !== null &&
|
|
329
|
+
(block.type === 'text' ||
|
|
330
|
+
block.type === 'tool_result' ||
|
|
331
|
+
block.type === 'image' ||
|
|
332
|
+
block.type === 'document'));
|
|
333
|
+
}
|
|
334
|
+
if (message.role === 'assistant') {
|
|
335
|
+
if (!Array.isArray(content))
|
|
336
|
+
return false;
|
|
337
|
+
return content.some((block) => typeof block === 'object' &&
|
|
338
|
+
block !== null &&
|
|
339
|
+
(block.type === 'text' ||
|
|
340
|
+
block.type === 'tool_use' ||
|
|
341
|
+
block.type === 'thinking'));
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
function claudeToolResultIds(entry) {
|
|
346
|
+
const ids = new Set();
|
|
347
|
+
if (entry.type !== 'user')
|
|
348
|
+
return ids;
|
|
349
|
+
for (const block of getClaudeContentBlocks(entry)) {
|
|
350
|
+
if (block.type === 'tool_result' && typeof block.tool_use_id === 'string') {
|
|
351
|
+
ids.add(block.tool_use_id);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return ids;
|
|
355
|
+
}
|
|
356
|
+
function claudeAssistantHasToolUse(entry, ids) {
|
|
357
|
+
if (entry.type !== 'assistant')
|
|
358
|
+
return false;
|
|
359
|
+
return getClaudeContentBlocks(entry).some((block) => block.type === 'tool_use' &&
|
|
360
|
+
typeof block.id === 'string' &&
|
|
361
|
+
ids.has(block.id));
|
|
362
|
+
}
|
|
363
|
+
/** Pulls the preserved-suffix boundary backward so a tool_use assistant entry
|
|
364
|
+
* and its tool_result user entry stay siblings: the suffix never opens with an
|
|
365
|
+
* orphaned tool_result and the compacted input never ends with a dangling
|
|
366
|
+
* tool_use. Same-response thinking/tool blocks live in one assistant entry,
|
|
367
|
+
* so entry-level cutting never splits them. */
|
|
368
|
+
function completeClaudeToolPairBoundary(activeEntries, startIndex) {
|
|
369
|
+
let boundary = startIndex;
|
|
370
|
+
while (boundary < activeEntries.length) {
|
|
371
|
+
const entry = activeEntries[boundary];
|
|
372
|
+
if (!entry)
|
|
373
|
+
break;
|
|
374
|
+
const ids = claudeToolResultIds(entry);
|
|
375
|
+
if (ids.size === 0)
|
|
376
|
+
break;
|
|
377
|
+
let extended = false;
|
|
378
|
+
for (let index = boundary - 1; index >= 0; index -= 1) {
|
|
379
|
+
const candidate = activeEntries[index];
|
|
380
|
+
if (candidate && claudeAssistantHasToolUse(candidate, ids)) {
|
|
381
|
+
boundary = index;
|
|
382
|
+
extended = true;
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (!extended)
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
return boundary;
|
|
390
|
+
}
|
|
391
|
+
/** Walks back from the active tail to the inclusive start of the recent suffix
|
|
392
|
+
* retained verbatim, then completes sibling adjacency at the boundary. */
|
|
393
|
+
function memoryPreservedSuffixStart(activeEntries) {
|
|
394
|
+
let start = activeEntries.length;
|
|
395
|
+
let textBearing = 0;
|
|
396
|
+
let estimatedTokens = 0;
|
|
397
|
+
for (let index = activeEntries.length - 1; index >= 0; index -= 1) {
|
|
398
|
+
const entry = activeEntries[index];
|
|
399
|
+
if (!entry)
|
|
400
|
+
continue;
|
|
401
|
+
start = index;
|
|
402
|
+
if (!isTextBearingClaudeEntry(entry))
|
|
403
|
+
continue;
|
|
404
|
+
textBearing += 1;
|
|
405
|
+
estimatedTokens += estimateModelRequestTokens(projectClaudeModelMessages([entry]));
|
|
406
|
+
if (textBearing >= MEMORY_COMPACT_MIN_SUFFIX_MESSAGES &&
|
|
407
|
+
(estimatedTokens >= MEMORY_COMPACT_MIN_SUFFIX_TOKENS || index === 0)) {
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return completeClaudeToolPairBoundary(activeEntries, start);
|
|
412
|
+
}
|
|
305
413
|
function validPromptSuggestion(value) {
|
|
306
414
|
const suggestion = value.trim();
|
|
307
415
|
if (!suggestion)
|
|
@@ -1464,6 +1572,7 @@ export class ClaudeSessionService {
|
|
|
1464
1572
|
let selectedEntries = activeEntries;
|
|
1465
1573
|
let logicalParentUuid = this.lastMessageUuid(activeEntries);
|
|
1466
1574
|
let preservedEntries = [];
|
|
1575
|
+
let memoryMessage;
|
|
1467
1576
|
if (selection) {
|
|
1468
1577
|
const targetIndex = activeEntries.findIndex((entry) => entry.uuid === selection.messageId);
|
|
1469
1578
|
if (targetIndex < 0) {
|
|
@@ -1486,8 +1595,18 @@ export class ClaudeSessionService {
|
|
|
1486
1595
|
: this.lastMessageUuid(selectedEntries);
|
|
1487
1596
|
}
|
|
1488
1597
|
}
|
|
1598
|
+
else {
|
|
1599
|
+
const memorySelection = await this.selectMemoryPreservedCompact(sessionId, activeEntries);
|
|
1600
|
+
if (memorySelection) {
|
|
1601
|
+
selectedEntries = [...memorySelection.compactedEntries];
|
|
1602
|
+
preservedEntries = [...memorySelection.preservedEntries];
|
|
1603
|
+
logicalParentUuid = memorySelection.logicalParentUuid;
|
|
1604
|
+
memoryMessage = memorySelection.memoryMessage;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1489
1607
|
const selectedMessages = projectClaudeModelMessages(selectedEntries);
|
|
1490
1608
|
const messages = [
|
|
1609
|
+
...(memoryMessage === undefined ? [] : [memoryMessage]),
|
|
1491
1610
|
...selectedMessages,
|
|
1492
1611
|
...(selection?.context
|
|
1493
1612
|
? [
|
|
@@ -1588,7 +1707,11 @@ export class ClaudeSessionService {
|
|
|
1588
1707
|
},
|
|
1589
1708
|
preservedUuids: preservedEntries.flatMap((entry) => typeof entry.uuid === 'string' ? [entry.uuid] : []),
|
|
1590
1709
|
}
|
|
1591
|
-
:
|
|
1710
|
+
: memoryMessage
|
|
1711
|
+
? {
|
|
1712
|
+
preservedUuids: preservedEntries.flatMap((entry) => typeof entry.uuid === 'string' ? [entry.uuid] : []),
|
|
1713
|
+
}
|
|
1714
|
+
: {}),
|
|
1592
1715
|
createUuid: () => uuids.shift() ?? randomUUID(),
|
|
1593
1716
|
});
|
|
1594
1717
|
const appendResult = await lease.appendMany(snapshot.tail, entries);
|
|
@@ -3279,6 +3402,59 @@ export class ClaudeSessionService {
|
|
|
3279
3402
|
return agentName;
|
|
3280
3403
|
return null;
|
|
3281
3404
|
}
|
|
3405
|
+
/** Conservative selective-preservation seam for manual compact: when the
|
|
3406
|
+
* durable session memory watermark matches an active entry, summarize the
|
|
3407
|
+
* last good memory artifact plus the post-watermark branch and retain a
|
|
3408
|
+
* recent suffix. Returns null to fall back to the existing full-compaction
|
|
3409
|
+
* behavior (missing/invalid watermark, empty projection, or oversized
|
|
3410
|
+
* projection). */
|
|
3411
|
+
async selectMemoryPreservedCompact(sessionId, activeEntries) {
|
|
3412
|
+
const controller = this.sessionMemoryController(sessionId);
|
|
3413
|
+
if (controller === null)
|
|
3414
|
+
return null;
|
|
3415
|
+
let watermark = null;
|
|
3416
|
+
let memorySummary = '';
|
|
3417
|
+
try {
|
|
3418
|
+
const [state, summary] = await Promise.all([
|
|
3419
|
+
controller.state(),
|
|
3420
|
+
controller.summary(),
|
|
3421
|
+
]);
|
|
3422
|
+
watermark = state.lastSummarizedMessageId;
|
|
3423
|
+
memorySummary = summary;
|
|
3424
|
+
}
|
|
3425
|
+
catch {
|
|
3426
|
+
return null;
|
|
3427
|
+
}
|
|
3428
|
+
if (watermark === null || watermark.length === 0)
|
|
3429
|
+
return null;
|
|
3430
|
+
if (memorySummary.trim().length === 0)
|
|
3431
|
+
return null;
|
|
3432
|
+
const watermarkIndex = activeEntries.findIndex((entry) => entry.uuid === watermark);
|
|
3433
|
+
if (watermarkIndex < 0)
|
|
3434
|
+
return null;
|
|
3435
|
+
const suffixStart = memoryPreservedSuffixStart(activeEntries);
|
|
3436
|
+
if (suffixStart <= 0 || suffixStart <= watermarkIndex + 1)
|
|
3437
|
+
return null;
|
|
3438
|
+
const compactedEntries = activeEntries.slice(watermarkIndex + 1, suffixStart);
|
|
3439
|
+
const preservedEntries = activeEntries.slice(suffixStart);
|
|
3440
|
+
const selectedMessages = projectClaudeModelMessages(compactedEntries);
|
|
3441
|
+
if (selectedMessages.length === 0)
|
|
3442
|
+
return null;
|
|
3443
|
+
const memoryMessage = { role: 'user', content: memorySummary };
|
|
3444
|
+
if (estimateModelRequestTokens([memoryMessage, ...selectedMessages]) >
|
|
3445
|
+
MEMORY_COMPACT_MAX_PROJECTION_TOKENS) {
|
|
3446
|
+
return null;
|
|
3447
|
+
}
|
|
3448
|
+
const logicalParentUuid = this.lastMessageUuid(activeEntries.slice(0, suffixStart));
|
|
3449
|
+
if (logicalParentUuid === null)
|
|
3450
|
+
return null;
|
|
3451
|
+
return {
|
|
3452
|
+
compactedEntries,
|
|
3453
|
+
preservedEntries,
|
|
3454
|
+
memoryMessage,
|
|
3455
|
+
logicalParentUuid,
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3282
3458
|
lastMessageUuid(entries) {
|
|
3283
3459
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
3284
3460
|
const entry = entries[index];
|