Skip to content
Open
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
22 changes: 20 additions & 2 deletions apps/cli/repo-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,20 @@ function defaultBranch(repoRoot: string): string {
const remoteHead = gitOptional(repoRoot, ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"]);
if (remoteHead?.startsWith("origin/")) return remoteHead.slice("origin/".length);

return "main";
// Network call — keep a short timeout so install/doctor/graph cannot hang on a bad remote.
const lsRemoteSymref = gitOptional(repoRoot, ["ls-remote", "--symref", "origin", "HEAD"], {
timeoutMs: 5_000,
});
const fromLsRemote = parseLsRemoteSymrefHead(lsRemoteSymref);
if (fromLsRemote !== undefined) return fromLsRemote;

return "unknown";
}

function parseLsRemoteSymrefHead(output: string | undefined): string | undefined {
if (output === undefined) return undefined;
const match = output.match(/^ref:\s+refs\/heads\/([^\s\t]+)/m);
return match?.[1];
}

function repoName(remoteUrl: string, repoRoot: string): string {
Expand All @@ -48,12 +61,17 @@ function repoName(remoteUrl: string, repoRoot: string): string {
return lastPart ?? basename(repoRoot);
}

function gitOptional(cwd: string, args: string[]): string | undefined {
function gitOptional(
cwd: string,
args: string[],
options: { timeoutMs?: number } = {},
): string | undefined {
try {
const output = execFileSync("git", args, {
cwd,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
...(options.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
}).trim();
return output.length > 0 ? output : undefined;
} catch {
Expand Down
13 changes: 11 additions & 2 deletions libs/hooks/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,13 @@ async function maybeUpdateWorkingMemory(attempt: ClaimedMemoryUpdateAttempt): Pr
}
}

function updateWorkingMemoryPrompt(
export function updateWorkingMemoryPrompt(
transcriptMarkdown: string,
attempt: ClaimedMemoryUpdateAttempt,
sessionRef: string,
proposalPath: string,
): string {
const escapedTranscript = escapeFilteredSessionTranscript(transcriptMarkdown.trim());
return `Run the greplica-update-working-memory skill for a completed coding-agent session. If your runtime supports slash-command skills, invoke /greplica-update-working-memory for this task.

Use the filtered session transcript below as the session context. It has been projected to Markdown with session metadata and human/agent text messages only.
Expand All @@ -128,11 +129,19 @@ Session:
- due_reason: ${attempt.reason}

<filtered_session_transcript>
${transcriptMarkdown.trim()}
${escapedTranscript}
</filtered_session_transcript>
`;
}

/** Neutralize literal closing tags so pasted transcript content cannot break the evidence fence. */
function escapeFilteredSessionTranscript(transcriptMarkdown: string): string {
return transcriptMarkdown.replaceAll(
"</filtered_session_transcript>",
"</\u200Bfiltered_session_transcript>",
);
}

function safePathSegment(value: string): string {
return value.toLowerCase().replace(/[^a-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || "unknown";
}
46 changes: 41 additions & 5 deletions libs/knowledge-graph/graph-context/context-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export interface BuildGraphContextOptions {
config?: GraphContextConfig;
repoRoot?: string;
resolveCodeAnchors?: boolean;
/** Optional embedder override (tests / hermetic callers). */
embedder?: Embedder;
}

interface ExistingEmbedding {
Expand All @@ -44,7 +46,7 @@ export class GraphContextBuilder {
const flowDocuments = buildFlowDocuments(graph);
const evidenceByClaim = buildEvidenceByClaim(graph);
const documents = [...claimDocuments, ...componentDocuments, ...flowDocuments];
const embedder = createEmbedder(config.embedding);
const embedder = options.embedder ?? createEmbedder(config.embedding);
const embeddingStatus = await this.ensureEmbeddings(repoId, documents, embedder, config);
if (options.warnOnCreatedEmbeddings && embeddingStatus.created > 0) {
console.warn(`graph context created ${embeddingStatus.created} missing embedding(s); proposal apply should normally pre-create them.`);
Expand Down Expand Up @@ -120,14 +122,23 @@ export class GraphContextBuilder {
};
}

async ensureForGraph(repoId: string, graph: GraphReadResult, config: GraphContextConfig = graphContextConfig): Promise<EmbeddingStatus> {
async ensureForGraph(
repoId: string,
graph: GraphReadResult,
config: GraphContextConfig = graphContextConfig,
embedder?: Embedder,
): Promise<EmbeddingStatus> {
const documents = [
...buildClaimDocuments(graph),
...buildComponentDocuments(graph),
...buildFlowDocuments(graph),
];
const embedder = createEmbedder(config.embedding);
return this.ensureEmbeddings(repoId, documents, embedder, config);
return this.ensureEmbeddings(
repoId,
documents,
embedder ?? createEmbedder(config.embedding),
config,
);
}

private async ensureEmbeddings(
Expand All @@ -149,6 +160,7 @@ export class GraphContextBuilder {
);
const missing = documents.filter((document) => !existing.has(document.key));
const vectors = await embedder.embedBatch(missing.map((document) => document.text));
assertEmbedBatchComplete(vectors, missing.length, config.embedding.dimensions);

repository.insertGraphObjectEmbeddings(
missing.map((document, index) => ({
Expand All @@ -158,7 +170,7 @@ export class GraphContextBuilder {
provider: config.embedding.provider,
model: config.embedding.model,
dimensions: config.embedding.dimensions,
embedding: float32ArrayToBuffer(vectors[index] ?? []),
embedding: float32ArrayToBuffer(vectors[index]),
})),
);

Expand Down Expand Up @@ -224,6 +236,30 @@ export class GraphContextBuilder {
}
}

function assertEmbedBatchComplete(
vectors: number[][],
expectedCount: number,
dimensions: number,
): void {
if (vectors.length !== expectedCount) {
throw new Error(
`embedBatch returned ${vectors.length} vector(s); expected ${expectedCount}.`,
);
}

for (let index = 0; index < vectors.length; index += 1) {
const vector = vectors[index];
if (vector === undefined || vector.length === 0) {
throw new Error(`embedBatch returned an empty vector at index ${index}.`);
}
if (vector.length !== dimensions) {
throw new Error(
`embedBatch vector at index ${index} has length ${vector.length}; expected ${dimensions}.`,
);
}
}
}

function buildEvidenceByClaim(graph: GraphReadResult): Map<string, ClaimEvidenceResult[]> {
const sources = new Map(graph.sources.map((source) => [source.id, source]));
const evidenceByClaim = new Map<string, ClaimEvidenceResult[]>();
Expand Down
20 changes: 20 additions & 0 deletions libs/storage/sqlite/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,26 @@ export class SqliteRepository implements GraphReadRepository {
}

ensureScope(input: CreateScopeInput): GraphScope {
// One main scope per repo: reuse and rename/ref-update if default_branch changes
// (e.g. fabricated "main" → "unknown" or ls-remote-resolved name) instead of
// creating orphan kind=main rows keyed by the new branch string.
if (input.kind === "main") {
const existingMain = this.db
.prepare(
"SELECT * FROM graph_scopes WHERE repo_id = ? AND kind = 'main' ORDER BY created_at LIMIT 1",
)
.get(input.repo_id) as GraphScope | undefined;
if (existingMain) {
if (existingMain.name !== input.name || existingMain.ref !== input.ref) {
this.db
.prepare("UPDATE graph_scopes SET name = ?, ref = ? WHERE id = ?")
.run(input.name, input.ref, existingMain.id);
return { ...existingMain, name: input.name, ref: input.ref };
}
return existingMain;
}
}

const existing = this.db
.prepare("SELECT * FROM graph_scopes WHERE repo_id = ? AND kind = ? AND name = ?")
.get(input.repo_id, input.kind, input.name) as GraphScope | undefined;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"smoke:copilot": "npm run build && node scripts/smoke-copilot-install.mjs",
"smoke:opencode": "npm run build && node scripts/smoke-opencode-install.mjs",
"smoke:cursor": "npm run build && node scripts/smoke-cursor-install.mjs",
"test": "npm run build && node scripts/check-transcript-bundle.js && node scripts/check-repo-context.js && node scripts/check-install-options.js && node scripts/check-repo-installations.js && node scripts/check-managed-cli.js && node scripts/check-graph-view.js && node scripts/check-graph-view-offline-browser.js && node scripts/check-source-memberships.js && node scripts/check-proposal-validate.js && node scripts/check-bm25-tokenizer.js && node scripts/check-anchor-drift.js && node scripts/check-find-similar-claims.js && node scripts/check-apply-proposal-dedupe.js && node scripts/check-opencode-sqlite-transcript.js",
"test": "npm run build && node scripts/check-transcript-bundle.js && node scripts/check-repo-context.js && node scripts/check-install-options.js && node scripts/check-repo-installations.js && node scripts/check-managed-cli.js && node scripts/check-graph-view.js && node scripts/check-graph-view-offline-browser.js && node scripts/check-source-memberships.js && node scripts/check-proposal-validate.js && node scripts/check-bm25-tokenizer.js && node scripts/check-anchor-drift.js && node scripts/check-find-similar-claims.js && node scripts/check-apply-proposal-dedupe.js && node scripts/check-opencode-sqlite-transcript.js && node scripts/check-embed-batch-length.js && node scripts/check-worker-transcript-escape.js && node scripts/check-main-scope-reuse.js",
"test:repo-installations": "npm run build && node scripts/check-repo-installations.js",
"test:transcript-bundle": "npm run build && node scripts/check-transcript-bundle.js",
"test:repo-context": "npm run build && node scripts/check-repo-context.js",
Expand Down
Loading