Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 75 additions & 7 deletions src/main/java/dev/talos/cli/modes/AssistantTurnExecutor.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
import dev.talos.runtime.context.ActiveTaskContextPolicy;
import dev.talos.runtime.context.ArtifactGoal;
import dev.talos.runtime.context.ChangeSummaryContext;
import dev.talos.runtime.context.ProjectMemoryContext;
import dev.talos.runtime.context.ProjectMemoryLimits;
import dev.talos.runtime.context.ProjectMemoryLoader;
import dev.talos.runtime.context.ProjectMemoryRequest;
import dev.talos.runtime.outcome.InspectUnderCompletionAnswerGuard;
import dev.talos.runtime.outcome.MutationFailureAnswerRenderer;
import dev.talos.runtime.outcome.NoToolAnswerTruthfulnessGuard;
Expand Down Expand Up @@ -212,9 +216,12 @@ public static TurnOutput execute(List<ChatMessage> messages, Path workspace,
activeDecisionUpdatesTurnSurface || workspaceBoundaryReplayedRequest);
CurrentTurnPlan currentTurnPlan = buildCurrentTurnPlan(taskContract, ctx, activeDecision);
recordPolicyTrace(currentTurnPlan, ctx);
ProjectMemoryContext projectMemory = loadProjectMemory(workspace, currentTurnPlan.taskContract());
injectProjectMemoryInstruction(messages, projectMemory);
injectTaskContractInstruction(messages, currentTurnPlan, true);
injectStaticVerificationRepairInstruction(messages, currentTurnPlan.taskContract(), workspace);
PromptAuditSnapshot promptAudit = recordPromptAudit(currentTurnPlan, messages, ctx);
recordProjectMemoryDiagnostics(projectMemory);
PromptAuditSnapshot promptAudit = recordPromptAudit(currentTurnPlan, messages, ctx, projectMemory);
recordPromptDebugDiagnostics(promptAudit);
emitPromptAuditIfEnabled(promptAudit, ctx);
Context turnContext = ctx;
Expand Down Expand Up @@ -1021,24 +1028,57 @@ private static PromptAuditSnapshot recordPromptAudit(
CurrentTurnPlan plan,
List<ChatMessage> messages,
Context ctx
) {
return recordPromptAudit(plan, messages, ctx, null);
}

private static PromptAuditSnapshot recordPromptAudit(
CurrentTurnPlan plan,
List<ChatMessage> messages,
Context ctx,
ProjectMemoryContext projectMemory
) {
PromptAuditSnapshot snapshot = PromptAuditSnapshot.fromPlan(
plan,
messages,
ctx == null || ctx.conversationManager() == null
? null
: ctx.conversationManager().lastCompactionStatus());
: ctx.conversationManager().lastCompactionStatus(),
projectMemory == null ? PromptAuditSnapshot.NOT_DERIVED : projectMemory.renderDiagnostic(),
memoryRetentionStatus(ctx));
LocalTurnTraceCapture.recordPromptAudit(snapshot);
return snapshot;
}

private static void recordPromptDebugDiagnostics(PromptAuditSnapshot snapshot) {
if (snapshot == null
|| snapshot.compactionStatus().isBlank()
|| PromptAuditSnapshot.NOT_DERIVED.equals(snapshot.compactionStatus())) {
return;
if (snapshot == null) return;
if (!snapshot.compactionStatus().isBlank()
&& !PromptAuditSnapshot.NOT_DERIVED.equals(snapshot.compactionStatus())) {
PromptDebugCapture.putTurnDiagnostic("compactionStatus", snapshot.compactionStatus());
}
if (!snapshot.memoryRetentionStatus().isBlank()
&& !PromptAuditSnapshot.NOT_DERIVED.equals(snapshot.memoryRetentionStatus())) {
PromptDebugCapture.putTurnDiagnostic("memoryRetentionStatus", snapshot.memoryRetentionStatus());
}
}

private static String memoryRetentionStatus(Context ctx) {
if (ctx == null || ctx.memory() == null) return PromptAuditSnapshot.NOT_DERIVED;
SessionMemory.RetentionEvictionStats stats = ctx.memory().retentionEvictionStats();
if (stats.rawTurnMessagesEvictedWithoutSketch() == 0 && stats.toolEvidenceEntriesEvicted() == 0) {
return "NONE";
}
return "rawTurnMessagesEvictedWithoutSketch=" + stats.rawTurnMessagesEvictedWithoutSketch()
+ " toolEvidenceEntriesEvicted=" + stats.toolEvidenceEntriesEvicted();
}

private static void recordProjectMemoryDiagnostics(ProjectMemoryContext projectMemory) {
if (projectMemory == null) return;
PromptDebugCapture.putTurnDiagnostic("projectMemoryStatus", projectMemory.renderDiagnostic());
String details = projectMemory.renderDebugDetails();
if (!details.isBlank()) {
PromptDebugCapture.putTurnDiagnostic("projectMemoryDetails", details);
}
PromptDebugCapture.putTurnDiagnostic("compactionStatus", snapshot.compactionStatus());
}

private static void emitPromptAuditIfEnabled(PromptAuditSnapshot snapshot, Context ctx) {
Expand Down Expand Up @@ -1162,6 +1202,22 @@ public static void injectTaskContractInstruction(List<ChatMessage> messages, Cur
injectTaskContractInstruction(messages, plan, false);
}

static void injectProjectMemoryInstruction(List<ChatMessage> messages, ProjectMemoryContext projectMemory) {
if (messages == null || messages.isEmpty() || projectMemory == null) return;
messages.removeIf(AssistantTurnExecutor::isProjectMemoryInstruction);
String rendered = projectMemory.renderForPrompt();
if (rendered.isBlank()) return;

int insertAt = 0;
for (int i = 0; i < messages.size(); i++) {
if ("system".equals(messages.get(i).role())) {
insertAt = i + 1;
break;
}
}
messages.add(insertAt, ChatMessage.system(rendered));
}

private static void injectTaskContractInstruction(
List<ChatMessage> messages,
CurrentTurnPlan plan,
Expand Down Expand Up @@ -1237,6 +1293,11 @@ private static List<String> defaultVisibleToolNames(TaskContract contract, Execu
return ToolSurfacePlanner.defaultVisibleToolNames(contract, phase);
}

private static ProjectMemoryContext loadProjectMemory(Path workspace, TaskContract contract) {
return new ProjectMemoryLoader(ProjectMemoryLimits.defaults())
.load(new ProjectMemoryRequest(workspace, null, contract));
}

static void injectStaticVerificationRepairInstruction(
List<ChatMessage> messages,
TaskContract taskContract
Expand Down Expand Up @@ -1353,6 +1414,13 @@ private static boolean isTaskContractInstruction(ChatMessage message) {
|| message.content().startsWith("[CurrentTurnCapability]"));
}

private static boolean isProjectMemoryInstruction(ChatMessage message) {
return message != null
&& "system".equals(message.role())
&& message.content() != null
&& message.content().startsWith("[ProjectMemory]");
}

private static boolean isStaticVerificationRepairInstruction(ChatMessage message) {
return message != null
&& "system".equals(message.role())
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/dev/talos/cli/prompt/PromptDebugInspector.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,24 @@ private static void appendDiagnostics(StringBuilder out, Map<String, String> dia
if (compactionStatus != null && !compactionStatus.isBlank()) {
out.append("- Compaction: ").append(compactionStatus).append('\n');
}
String memoryRetentionStatus = diagnostics.get("memoryRetentionStatus");
if (memoryRetentionStatus != null && !memoryRetentionStatus.isBlank()) {
out.append("- Memory retention (cumulative this session): ").append(memoryRetentionStatus).append('\n');
}
String projectMemoryStatus = diagnostics.get("projectMemoryStatus");
if (projectMemoryStatus != null && !projectMemoryStatus.isBlank()) {
out.append("- Project memory: ").append(projectMemoryStatus).append('\n');
}
String projectMemoryDetails = diagnostics.get("projectMemoryDetails");
if (projectMemoryDetails != null && !projectMemoryDetails.isBlank()) {
out.append("\n## Project Memory\n\n");
for (String line : projectMemoryDetails.split("\\R")) {
if (!line.isBlank()) {
out.append("- ").append(line.strip()).append('\n');
}
}
out.append('\n');
}
}

private static void appendContextLedger(StringBuilder out) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,10 @@ private static void appendPromptAudit(StringBuilder sb, dev.talos.runtime.trace.
.append(" messages=").append(audit.historyMessageCount())
.append('\n');
sb.append(" compaction: ").append(blankDefault(audit.compactionStatus(), "NOT_DERIVED")).append('\n');
sb.append(" projectMemory: ").append(blankDefault(audit.projectMemoryStatus(), "NOT_DERIVED")).append('\n');
sb.append(" memoryRetentionCumulative: ")
.append(blankDefault(audit.memoryRetentionStatus(), "NOT_DERIVED"))
.append('\n');
sb.append(" currentTurnFrame: ")
.append(audit.currentTurnFrameInjected() ? "injected " : "not-injected ")
.append(blankDefault(audit.currentTurnFramePlacement(), "UNKNOWN"));
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/dev/talos/core/context/ContextItemSource.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ public enum ContextItemSource {
SYSTEM_FRAME,
TOOL_RESULT,
RAG_SNIPPET,
SYMBOL_HIT,
SESSION_MEMORY,
PROJECT_MEMORY,
COMMAND_OUTPUT,
PROMPT_DEBUG,
TRACE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/** Trust boundary that produced or carried a context item. */
public enum ExecutionBoundary {
LOCAL_WORKSPACE,
LOCAL_USER_CONFIGURATION,
LOCAL_RUNTIME_ARTIFACT,
RAG_INDEX,
SESSION_MEMORY,
Expand Down
43 changes: 41 additions & 2 deletions src/main/java/dev/talos/core/index/Indexer.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
Expand All @@ -47,7 +48,7 @@ public class Indexer {
private static final Logger LOG = LoggerFactory.getLogger(Indexer.class);
private static final boolean IS_WINDOWS = System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("windows");
private static final ObjectMapper JSON = new ObjectMapper();
private static final int INDEX_METADATA_SCHEMA_VERSION = 2;
private static final int INDEX_METADATA_SCHEMA_VERSION = 3;

private final Config cfg;
private volatile IndexingStats lastRunStats;
Expand Down Expand Up @@ -166,6 +167,14 @@ public void index(Path root, boolean forceFullReindex, IndexProgressListener lis
LOG.info("Matched {} files after include/exclude filters.", files.size());
}

final Path indexDir = indexDirFor(rootPath);
final Map<String, List<SymbolHit>> existingSymbolsByPath = symbolsByPath(SymbolIndexStore.load(indexDir));
final ConcurrentHashMap<String, List<SymbolHit>> refreshedSymbolsByPath = new ConcurrentHashMap<>();
final Set<String> currentRelPaths = ConcurrentHashMap.newKeySet();
for (Path file : files) {
currentRelPaths.add(rootPath.relativize(file).toString().replace('\\', '/'));
}

// Vectors toggle (BM25-only fallback if disabled or probe fails)
boolean vecEnabled = true;
Object vectorsObj = rag.get("vectors");
Expand Down Expand Up @@ -202,7 +211,7 @@ public void index(Path root, boolean forceFullReindex, IndexProgressListener lis
// Effectively-final reference for lambdas
final CachingEmbeddings embForTasks = useVectors ? cachedEmb : null;

try (var store = new LuceneStore(indexDirFor(rootPath), vectorDim)) {
try (var store = new LuceneStore(indexDir, vectorDim)) {
int chunkChars = CfgUtil.intAt(rag, "chunk_chars", 1200);
int overlap = CfgUtil.intAt(rag, "chunk_overlap", 150);

Expand Down Expand Up @@ -233,6 +242,7 @@ public void index(Path root, boolean forceFullReindex, IndexProgressListener lis
String text = parseIndexableText(rootPath, p);
stats.addParseTime(System.currentTimeMillis() - parseStart);
stats.incrementFilesEmbedded();
refreshedSymbolsByPath.put(rel, SymbolExtractor.extract(rel, text));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Populate symbol sidecars for unchanged files

When a user runs the normal /reindex or talos index path after upgrading from an index that has Lucene chunks but no talos-symbols.json, unchanged files hit the existing store.isUpToDate(...) early return before this map is populated. Because existingSymbolsByPath is empty in that migration case, writeMergedSymbolIndex(...) later writes an empty sidecar even though all code files are already indexed, so exact symbol retrieval stays disabled until the user knows to force a full reindex.

Useful? React with 👍 / 👎.


List<ParsedChunk> chunks = Chunker.chunk(rel, text, chunkChars, overlap);

Expand Down Expand Up @@ -338,6 +348,7 @@ public void index(Path root, boolean forceFullReindex, IndexProgressListener lis

long commitStart = System.currentTimeMillis();
store.commit();
writeMergedSymbolIndex(indexDir, existingSymbolsByPath, refreshedSymbolsByPath, currentRelPaths);
writePolicyMetadata(rootPath);
stats.addCommitTime(System.currentTimeMillis() - commitStart);

Expand Down Expand Up @@ -366,6 +377,34 @@ private static List<String> firstNonEmptyStrList(List<String> a, List<String> b)
return (b == null) ? List.of() : b;
}

private static Map<String, List<SymbolHit>> symbolsByPath(List<SymbolHit> hits) {
Map<String, List<SymbolHit>> byPath = new LinkedHashMap<>();
if (hits == null) return byPath;
for (SymbolHit hit : hits) {
if (hit == null || hit.path().isBlank()) continue;
byPath.computeIfAbsent(hit.path(), ignored -> new ArrayList<>()).add(hit);
}
return byPath;
}

private static void writeMergedSymbolIndex(
Path indexDir,
Map<String, List<SymbolHit>> existingSymbolsByPath,
Map<String, List<SymbolHit>> refreshedSymbolsByPath,
Set<String> currentRelPaths
) throws IOException {
List<SymbolHit> merged = new ArrayList<>();
for (String path : currentRelPaths) {
List<SymbolHit> refreshed = refreshedSymbolsByPath.get(path);
if (refreshed != null) {
merged.addAll(refreshed);
} else {
merged.addAll(existingSymbolsByPath.getOrDefault(path, List.of()));
}
}
SymbolIndexStore.writeAll(indexDir, merged);
}

/**
* Reindex the given workspace root. Delegates directly to {@link #index(Path)}.
* Returns a status string for callers that display a summary.
Expand Down
Loading