@sema-agent/core 1.436.2 → 1.437.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.
@@ -66,11 +66,14 @@ export interface MaybeCompactOptions {
66
66
  maxFiles?: number;
67
67
  maxCharsPerFile?: number;
68
68
  recentlyReadFiles?: () => string[];
69
+ instructionSourcePaths?: ReadonlyArray<string>;
70
+ normalizePath?: (raw: string) => Promise<string>;
71
+ isDedupStubResult?: (resultText: string) => boolean;
69
72
  };
70
73
  onApplied?: (attachedComplete: ReadonlyArray<{
71
74
  path: string;
72
75
  content: string;
73
- }>) => void;
76
+ }>, preserveReadState?: ReadonlyArray<string>) => void;
74
77
  overheadTokens?: number;
75
78
  summaryProvider?: (input: {
76
79
  messagesToSummarize: AgentMessage[];
@@ -1,4 +1,5 @@
1
1
  import { DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, CompactionError, compact, computeFileLists, dryRunSummarizationClamp, estimateContextTokens, estimateTokens, prepareCompaction, shouldCompact, } from "../internal/harness.js";
2
+ import { fileArgPath } from "../tools/fs/safety.js";
2
3
  import { contextEditFrontier } from "./context-edit.js";
3
4
  import { selectCompactionEpoch } from "../prompt-assembly/epoch.js";
4
5
  import { SEMA_DEFAULT_PACK } from "../prompt-assembly/packs/sema-default.js";
@@ -318,9 +319,95 @@ export async function maybeCompact(opts) {
318
319
  const attachedComplete = [];
319
320
  const att = opts.workingFileAttachments;
320
321
  const recentlyRead = att?.recentlyReadFiles?.() ?? [];
321
- const candidateFiles = recentlyRead.length > 0 ? recentlyRead : (details?.modifiedFilesByRecency ?? details?.modifiedFiles);
322
+ const rawCandidateFiles = recentlyRead.length > 0 ? recentlyRead : (details?.modifiedFilesByRecency ?? details?.modifiedFiles);
323
+ const maxFilesForSelection = Math.max(1, att?.maxFiles ?? 3);
324
+ const excludedReadStatePreserveKeys = [];
325
+ const candidateFiles = Array.isArray(rawCandidateFiles) && rawCandidateFiles.length > 0
326
+ ? await (async () => {
327
+ const keptTailReadPathsRaw = new Set();
328
+ const firstKeptIdx = branch.findIndex((e) => e.id === firstKeptEntryId);
329
+ if (firstKeptIdx >= 0) {
330
+ const dedupStub = att?.isDedupStubResult;
331
+ for (let i = firstKeptIdx; i < branch.length; i++) {
332
+ const entry = branch[i];
333
+ if (entry.type !== "message" || entry.message.role !== "assistant")
334
+ continue;
335
+ const msg = entry.message;
336
+ if (!("content" in msg) || !Array.isArray(msg.content))
337
+ continue;
338
+ const pendingReads = new Map();
339
+ for (const block of msg.content) {
340
+ if (typeof block !== "object" || block === null)
341
+ continue;
342
+ if (!("type" in block) || block.type !== "toolCall")
343
+ continue;
344
+ const tc = block;
345
+ if (typeof tc.name !== "string" || typeof tc.id !== "string" || !tc.arguments)
346
+ continue;
347
+ if (tc.name !== "read" && tc.name !== "Read" && tc.name !== "read_file")
348
+ continue;
349
+ const path = fileArgPath(tc.arguments);
350
+ if (path)
351
+ pendingReads.set(tc.id, path);
352
+ }
353
+ if (pendingReads.size === 0)
354
+ continue;
355
+ for (let j = i + 1; j < branch.length && pendingReads.size > 0; j++) {
356
+ const next = branch[j];
357
+ if (next.type !== "message")
358
+ continue;
359
+ if (next.message.role === "assistant")
360
+ break;
361
+ if (next.message.role !== "toolResult")
362
+ continue;
363
+ const tr = next.message;
364
+ const path = pendingReads.get(tr.toolCallId);
365
+ if (path === undefined)
366
+ continue;
367
+ pendingReads.delete(tr.toolCallId);
368
+ if (tr.isError)
369
+ continue;
370
+ if (dedupStub) {
371
+ const text = tr.content.map((c) => ("text" in c ? c.text : "")).join("");
372
+ if (dedupStub(text))
373
+ continue;
374
+ }
375
+ keptTailReadPathsRaw.add(path);
376
+ }
377
+ }
378
+ }
379
+ const instructionPathsRaw = new Set(att?.instructionSourcePaths ?? []);
380
+ if (keptTailReadPathsRaw.size === 0 && instructionPathsRaw.size === 0)
381
+ return rawCandidateFiles;
382
+ const normalize = att?.normalizePath;
383
+ if (!normalize) {
384
+ excludedReadStatePreserveKeys.push(...keptTailReadPathsRaw, ...instructionPathsRaw);
385
+ return rawCandidateFiles.filter((p) => !keptTailReadPathsRaw.has(p) && !instructionPathsRaw.has(p));
386
+ }
387
+ const normalizeSafe = async (p) => {
388
+ try {
389
+ return await normalize(p);
390
+ }
391
+ catch {
392
+ return p;
393
+ }
394
+ };
395
+ const normalizeCap = Math.max(maxFilesForSelection * 4, 12);
396
+ const boundedCandidates = rawCandidateFiles.slice(0, normalizeCap);
397
+ const overflowCandidates = rawCandidateFiles.slice(normalizeCap);
398
+ const [normBounded, normKeptTail, normInstruction] = await Promise.all([
399
+ Promise.all(boundedCandidates.map(normalizeSafe)),
400
+ Promise.all([...keptTailReadPathsRaw].map(normalizeSafe)),
401
+ Promise.all([...instructionPathsRaw].map(normalizeSafe)),
402
+ ]);
403
+ excludedReadStatePreserveKeys.push(...normKeptTail, ...normInstruction);
404
+ const excluded = new Set([...normKeptTail, ...normInstruction]);
405
+ const filteredBounded = boundedCandidates.filter((_, i) => !excluded.has(normBounded[i]));
406
+ return [...filteredBounded, ...overflowCandidates];
407
+ })()
408
+ : rawCandidateFiles;
322
409
  if (att && Array.isArray(candidateFiles) && candidateFiles.length > 0) {
323
- const maxFiles = Math.max(1, att.maxFiles ?? 3);
410
+ const maxFiles = maxFilesForSelection;
324
411
  const perFileCap = Math.max(200, att.maxCharsPerFile ?? 16_000);
325
412
  let remaining = Math.min(maxFiles * perFileCap, Math.floor(window * 0.15) * cpt);
326
413
  const blocks = [];
@@ -369,7 +456,7 @@ export async function maybeCompact(opts) {
369
456
  ...(restatedListings !== undefined ? { announcedListings: restatedListings } : {}),
370
457
  }, false);
371
458
  try {
372
- opts.onApplied?.(attachedComplete);
459
+ opts.onApplied?.(attachedComplete, excludedReadStatePreserveKeys);
373
460
  }
374
461
  catch {
375
462
  }
@@ -194,10 +194,12 @@ export interface Prepared {
194
194
  promptOverheadTokens: number;
195
195
  readTaskFile?: (path: string) => Promise<string | null>;
196
196
  recentlyReadFiles?: () => string[];
197
+ normalizeAttachmentPath?: (raw: string) => Promise<string>;
198
+ isDedupStubResult?: (resultText: string) => boolean;
197
199
  onCompactionApplied?: (attachedComplete: ReadonlyArray<{
198
200
  path: string;
199
201
  content: string;
200
- }>) => void;
202
+ }>, preserveReadState?: ReadonlyArray<string>) => void;
201
203
  lspDiagnostics?: {
202
204
  registry: import("../lsp-diagnostics.js").LspDiagnosticsRegistry;
203
205
  nudge: (rawPath: string) => void;
@@ -55,7 +55,7 @@ import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.j
55
55
  import { createTaskOutputTool, createTaskStopTool, defaultTaskRegistry } from "../task-registry.js";
56
56
  import { createMonitorTool } from "../../tools/monitor.js";
57
57
  import { createWorktreeTools } from "../../tools/worktree.js";
58
- import { applyCompactionToReadFileState, bashReversibilityProbe, createHandsToolkit, seedReadFileStateFromContext, HAND_TOOL_EFFECTS, pdfModelCapabilitiesOf } from "../../tools/fs/index.js";
58
+ import { applyCompactionToReadFileState, bashReversibilityProbe, createHandsToolkit, isReadDedupStubResult, seedReadFileStateFromContext, HAND_TOOL_EFFECTS, pdfModelCapabilitiesOf } from "../../tools/fs/index.js";
59
59
  import { decodeTextBytes } from "../../tools/fs/encoding.js";
60
60
  import { ASK_USER_QUESTION_TOOL_NAME, createAskUserQuestionTool } from "../ask-question.js";
61
61
  import { createSchedulerTools } from "../../tools/scheduler-tools.js";
@@ -3291,6 +3291,20 @@ export async function prepareTask(spec, deps, sessions, resume, _memorySelector,
3291
3291
  }
3292
3292
  }
3293
3293
  : undefined;
3294
+ const normalizeAttachmentPath = handsEnabled
3295
+ ? async (path) => {
3296
+ if (attachmentRootCanonical === undefined)
3297
+ return path;
3298
+ try {
3299
+ const rk = await resolveKey(executionEnv, attachmentRootCanonical, path, undefined, handsCwdRef?.current, additionalRootsCanonical);
3300
+ return rk.ok ? rk.key : path;
3301
+ }
3302
+ catch {
3303
+ return path;
3304
+ }
3305
+ }
3306
+ : undefined;
3307
+ const isDedupStubResult = handsEnabled ? isReadDedupStubResult : undefined;
3294
3308
  const recentlyReadFiles = handsEnabled && readFileStateForCheckpoint
3295
3309
  ? () => [...readFileStateForCheckpoint.entries()]
3296
3310
  .filter(([, v]) => v.seededFromContext !== true)
@@ -3298,7 +3312,7 @@ export async function prepareTask(spec, deps, sessions, resume, _memorySelector,
3298
3312
  .map(([path]) => path)
3299
3313
  : undefined;
3300
3314
  const onCompactionApplied = handsEnabled && readFileStateForCheckpoint
3301
- ? (attachedComplete) => applyCompactionToReadFileState(readFileStateForCheckpoint, attachedComplete)
3315
+ ? (attachedComplete, preserveReadState) => applyCompactionToReadFileState(readFileStateForCheckpoint, attachedComplete, preserveReadState)
3302
3316
  : undefined;
3303
3317
  const detectExternalChanges = handsEnabled && readFileStateForCheckpoint
3304
3318
  ? async (maxFiles) => {
@@ -3382,7 +3396,7 @@ export async function prepareTask(spec, deps, sessions, resume, _memorySelector,
3382
3396
  : undefined;
3383
3397
  overheadState.promptChars = systemPrompt.length;
3384
3398
  const preparedHolder = {};
3385
- const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, cacheBreakDetector, cacheFingerprint, promptManifest, epochDeclaredSections, activeTools, ownedEnv, suspendRef, reviewRef, reviewRequestRef, suspendLoopRef, suspendForResource, ...(callCapRef ? { callCapRef } : {}), ...(cutKills ? { cutKills } : {}), ...(callCapRef ? { callIssuedAtRef } : {}), suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, promptOverheadTokens, readTaskFile, recentlyReadFiles, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
3399
+ const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, cacheBreakDetector, cacheFingerprint, promptManifest, epochDeclaredSections, activeTools, ownedEnv, suspendRef, reviewRef, reviewRequestRef, suspendLoopRef, suspendForResource, ...(callCapRef ? { callCapRef } : {}), ...(cutKills ? { cutKills } : {}), ...(callCapRef ? { callIssuedAtRef } : {}), suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, promptOverheadTokens, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
3386
3400
  const prepared = buildPrepared();
3387
3401
  preparedHolder.current = prepared;
3388
3402
  return prepared;
@@ -1823,6 +1823,11 @@ export class Runner {
1823
1823
  ? {
1824
1824
  readFile: prepared.readTaskFile,
1825
1825
  ...(prepared.recentlyReadFiles ? { recentlyReadFiles: prepared.recentlyReadFiles } : {}),
1826
+ ...(prepared.normalizeAttachmentPath ? { normalizePath: prepared.normalizeAttachmentPath } : {}),
1827
+ ...(prepared.isDedupStubResult ? { isDedupStubResult: prepared.isDedupStubResult } : {}),
1828
+ ...(prepared.instructionSources
1829
+ ? { instructionSourcePaths: prepared.instructionSources.filter((s) => s.contentHash !== null).map((s) => s.path) }
1830
+ : {}),
1826
1831
  ...(typeof spec.compaction?.attachWorkingFiles === "object" ? spec.compaction.attachWorkingFiles : undefined),
1827
1832
  }
1828
1833
  : undefined,
@@ -2257,6 +2262,11 @@ export class Runner {
2257
2262
  ? {
2258
2263
  readFile: prepared.readTaskFile,
2259
2264
  ...(prepared.recentlyReadFiles ? { recentlyReadFiles: prepared.recentlyReadFiles } : {}),
2265
+ ...(prepared.normalizeAttachmentPath ? { normalizePath: prepared.normalizeAttachmentPath } : {}),
2266
+ ...(prepared.isDedupStubResult ? { isDedupStubResult: prepared.isDedupStubResult } : {}),
2267
+ ...(prepared.instructionSources
2268
+ ? { instructionSourcePaths: prepared.instructionSources.filter((s) => s.contentHash !== null).map((s) => s.path) }
2269
+ : {}),
2260
2270
  ...(typeof spec.compaction?.attachWorkingFiles === "object" ? spec.compaction.attachWorkingFiles : undefined),
2261
2271
  }
2262
2272
  : undefined,
@@ -3539,6 +3549,11 @@ export class Runner {
3539
3549
  ? {
3540
3550
  readFile: prepared.readTaskFile,
3541
3551
  ...(prepared.recentlyReadFiles ? { recentlyReadFiles: prepared.recentlyReadFiles } : {}),
3552
+ ...(prepared.normalizeAttachmentPath ? { normalizePath: prepared.normalizeAttachmentPath } : {}),
3553
+ ...(prepared.isDedupStubResult ? { isDedupStubResult: prepared.isDedupStubResult } : {}),
3554
+ ...(prepared.instructionSources
3555
+ ? { instructionSourcePaths: prepared.instructionSources.filter((s) => s.contentHash !== null).map((s) => s.path) }
3556
+ : {}),
3542
3557
  ...(typeof spec.compaction?.attachWorkingFiles === "object" ? spec.compaction.attachWorkingFiles : undefined),
3543
3558
  }
3544
3559
  : undefined,
@@ -10,11 +10,12 @@ export declare const MAX_EDIT_BYTES: number;
10
10
  export type ReadImageDownsamplerOption = ImageDownsampler | false | undefined;
11
11
  export declare function msTimeoutToSec(timeoutMs: number | undefined): number;
12
12
  export declare function seededFileUnchangedReminder(filePath: string): string;
13
+ export declare function isReadDedupStubResult(resultText: string): boolean;
13
14
  export declare function seedReadFileStateFromContext(state: ReadFileState, key: string, content: string): void;
14
15
  export declare function applyCompactionToReadFileState(state: ReadFileState, attachedComplete: ReadonlyArray<{
15
16
  path: string;
16
17
  content: string;
17
- }>): void;
18
+ }>, preserveKeys?: ReadonlyArray<string>): void;
18
19
  export interface CwdRef {
19
20
  current: string;
20
21
  }
@@ -102,12 +102,17 @@ function countLines(s) {
102
102
  export function seededFileUnchangedReminder(filePath) {
103
103
  return `<system-reminder>This file is already in your context (see "Contents of ${filePath}" above) and has not changed on disk. Use that content instead of re-reading.</system-reminder>`;
104
104
  }
105
+ export function isReadDedupStubResult(resultText) {
106
+ return (resultText.includes("unchanged since you last read it") ||
107
+ resultText.includes("has not changed on disk. Use that content instead of re-reading"));
108
+ }
105
109
  export function seedReadFileStateFromContext(state, key, content) {
106
110
  state.set(key, { hash: sha256(content), totalLines: countLines(content), truncated: false, lastReadAt: Date.now(), seededFromContext: true });
107
111
  }
108
- export function applyCompactionToReadFileState(state, attachedComplete) {
112
+ export function applyCompactionToReadFileState(state, attachedComplete, preserveKeys = []) {
113
+ const preserve = new Set(preserveKeys);
109
114
  for (const [k, v] of [...state]) {
110
- if (v.seededFromContext !== true)
115
+ if (v.seededFromContext !== true && !preserve.has(k))
111
116
  state.delete(k);
112
117
  }
113
118
  for (const f of attachedComplete) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "1.436.2",
3
+ "version": "1.437.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",