2026-05-18 13:38:06 +02:00
import { cp , mkdir , readFile , rm , writeFile } from 'node:fs/promises' ;
2026-05-10 23:12:26 +02:00
import { dirname , join } from 'node:path' ;
import pLimit from 'p-limit' ;
import { z } from 'zod' ;
chore(workspace): gate dead-code with knip production mode (#196)
* refactor(workspace): relocate @ktx/llm source into packages/cli/src/llm
* refactor(workspace): rewrite @ktx/llm imports to relative paths
* refactor(workspace): fold internal packages into cli
* chore(workspace): gate dead-code with knip production mode
Turn on production-mode knip plus an autofix run in pre-commit and the
`pnpm dead-code` script, document the `/** @internal */` convention for
test-only exports in AGENTS.md, annotate test-only exports across the
CLI with that JSDoc, and drop dead exports/wrappers the new gate
surfaced (e.g. `cli-project.ts`, `lookerRuntimeSourceToFileAdapterSource`,
`createLocalScanEnrichmentProvidersFromConfig`,
`PGLITE_OWNER_PROCESS_BACKEND_CAPABILITIES`, stale type re-exports).
Replace the loose `ignoreIssues` allowlist in `knip.json` with explicit
production entries so cross-package barrel leaks are caught.
* refactor(cli): delete internal barrel index.ts files
The 34 `index.ts` re-export barrels inside `packages/cli/src/` were
holdovers from the pre-fold multi-workspace structure. Post-fold-in they
served no production purpose: external consumers go through the single
package main entry, and in-repo callers mostly imported through them
only because the path was short. Internally, knip flagged most barrel
re-exports as production-dead (only reached via tests).
This change:
- Deletes every internal barrel except `packages/cli/src/index.ts`
(the published package entry).
- Rewrites ~270 source/test files to import each name directly from
the file that defines it.
- Moves `tools/warehouse-verification/index.ts` to
`create-warehouse-verification-tools.ts` (the function it defined
locally) and updates its single consumer.
- Renames `search/backend-conformance.ts` → `.test-utils.ts` to match
the existing test-helper file convention.
- Deletes 13 dead test-only chains (dbt-descriptions/*,
live-database/extracted-schema, live-database/structural-sync,
relationship-* feedback/review chain) plus their tests and a
cascading orphan integration test.
- Updates test mocks that pointed at deleted barrel paths
(notion-client, connector barrels in scan/local-scan-connectors
tests) to mock the source files instead.
- Points the maintainer benchmark script
(`scripts/relationship-benchmark-report.mjs`) at source files
instead of `dist/context/scan/index.js`.
- Drops the barrel `!` entries from `knip.json`; adds explicit
production entries only for the benchmark code reached via dist by
the maintainer script.
Net: 413 files changed, ~1.2k insertions, ~9.4k deletions.
`pnpm run dead-code` (Biome + knip default + knip production) and
`pnpm run type-check` are clean; 2277 tests pass.
* refactor(workspace): rename @ktx/cli to @kaelio/ktx and pack it directly
Promote the CLI workspace package to the public name `@kaelio/ktx` and
drop the separate `scripts/build-public-npm-package.mjs` wrapper. The
CLI package is now publishable in place (`publishConfig.access: public`,
`provenance: true`), so artifact packing uses `pnpm pack` against
`packages/cli/` instead of assembling a parallel package tree.
Updates all workspace filter invocations, docs, tests, and release
readiness checks to reference the new package name, and folds the
tarball-name helper into `scripts/public-npm-release-metadata.mjs`.
* docs: align "agent clients" and "data agents" terminology
Replace "client agents" with "agent clients" and "database agents" with
"data agents" across AGENTS.md, README.md, the docs-site copy, and the
matching setup-agents test description, matching the canonical
vocabulary in docs/terminology.md.
Also moves packages/cli/tsconfig.json's tsBuildInfoFile from
node_modules/.cache/ to dist/.tsbuildinfo so incremental builds survive
node_modules reinstalls.
* refactor(release): single source of truth for package version
Make packages/cli/package.json the single source of truth for the
@kaelio/ktx version. publicNpmPackageVersion() now reads it directly,
so artifact filenames, release-readiness checks, and the Python wheel
version all derive from one field. The duplicate
release-policy.json.publicNpmPackageVersion is removed.
Previously the two fields could drift: tarballs were named
kaelio-ktx-0.4.1.tgz while internally containing
@kaelio/ktx@0.0.0-private.
- update-public-release-version.mjs rewrites both Python pyproject.toml
files (ktx-daemon, ktx-sl) alongside the npm package.jsons,
normalizing the version for PEP 440 (e.g. 0.1.0-rc.2 -> 0.1.0rc2).
- semantic-release-config.cjs adds the two pyproject.toml files to
@semantic-release/git assets so the release commit back to main
carries every version source in lockstep.
- The six "?? '0.0.0-private'" fallback literals across the CLI are
replaced with "?? getKtxCliPackageInfo().version", and
createDefaultKtxMcpServer makes its version arg required.
- docs/release.md describes the actual commit-back model: the dev tree
always reflects the most recent release; no sentinel pin to
maintain.
Verified: pnpm run artifacts:build now produces
kaelio-ktx-0.4.1.tgz and kaelio_ktx-0.4.1-py3-none-any.whl with
@kaelio/ktx@0.4.1 inside. Full type-check, dead-code, and
2287 vitests + 173 script tests pass.
* refactor(cli): inject embedding provider resolution and detect sentence-transformers runtime
Make resolveProjectEmbeddingProvider and runtimeIo injectable in ingest and
scan command entrypoints so tests can stub them, and teach
resolvePublicIngestRuntimeRequirements to flag the local-embeddings runtime
feature when ktx.yaml selects sentence-transformers.
* chore(cli): mark buildLocalStatsStatus and LocalStatsStatus as @internal
Both symbols are consumed only by status-project.test.ts. Annotating with
/** @internal */ keeps knip's production-mode check clean without changing
runtime behavior.
* fix(cli): use real package metadata in print-command-tree
The stubbed package name embedded a forbidden product identifier that
tripped the boundary check in CI. Read the metadata from package.json
instead — keeps the rendered tree unchanged and removes a duplicate
source of truth.
* feat(cli): show embedding coverage in `ktx status`, drop duplicate disk counts
Inline `(N embedded)` next to the Wiki scope counts and Semantic-layer
source counts, computed with `SUM(embedding_json IS NOT NULL)` over
`knowledge_pages` and `local_sl_sources`. Rename the "Knowledge" label to
"Wiki" (canonical per `docs/terminology.md`) and rename the matching
`localStats.knowledgePages` field to `localStats.wikiPages`.
Drop `wiki=N md` and `semantic-layer=N yaml` from the Disk row — those
duplicated the per-surface rows above. Disk now reports only actual byte
usage (db, cache, raw-sources). The unused `wikiGlobalMarkdownCount` /
`semanticLayerYamlCount` fields, the `isMarkdownEntry` / `isYamlEntry`
helpers, and the `filter` arg on `summarizeDir` are removed.
2026-05-21 15:28:58 +02:00
import { type KtxLogger , noopLogger } from '../../context/core/config.js' ;
import { createRuntimeToolDescriptorFromAiTool } from '../../context/llm/runtime-tools.js' ;
import type { KtxRuntimeToolSet } from '../../context/llm/runtime-port.js' ;
import type { CaptureSession , MemoryAction } from '../../context/memory/types.js' ;
import type { SemanticLayerService } from '../../context/sl/semantic-layer.service.js' ;
import type { SemanticLayerSource } from '../../context/sl/types.js' ;
import type { SlValidationDeps } from '../../context/sl/tools/sl-warehouse-validation.js' ;
import { createTouchedSlSources , type TouchedSlSource } from '../../context/tools/touched-sl-sources.js' ;
import type { ToolContext } from '../../context/tools/base-tool.js' ;
import type { ToolSession } from '../../context/tools/tool-session.js' ;
import type { KnowledgeWikiService } from '../../context/wiki/knowledge-wiki.service.js' ;
2026-05-17 10:10:14 +02:00
import { findDanglingWikiRefsForActions } from '../wiki/wiki-ref-validation.js' ;
2026-05-10 23:12:26 +02:00
import { actionTargetConnectionId } from './action-identity.js' ;
2026-05-12 16:56:58 -04:00
import { NOTION_DEFAULT_MAX_KNOWLEDGE_CREATES_PER_RUN } from './adapters/notion/types.js' ;
2026-05-18 13:38:06 +02:00
import { validateFinalIngestArtifacts , validateProvenanceRawPaths } from './artifact-gates.js' ;
2026-05-10 23:12:26 +02:00
import { selectRelevantCanonicalPins } from './canonical-pins.js' ;
2026-05-18 13:38:06 +02:00
import { finalGateRepairPaths , repairFinalGateFailure } from './final-gate-repair.js' ;
2026-05-20 14:17:10 +02:00
import {
compareFinalizationDeclarations ,
deriveFinalizationTouchedSources ,
deriveFinalizationWikiPageKeys ,
} from './finalization-scope.js' ;
2026-05-18 13:38:06 +02:00
import { FileIngestTraceWriter , ingestTracePathForJob , type IngestTraceWriter , traceTimed } from './ingest-trace.js' ;
import { integrateWorkUnitPatch } from './isolated-diff/patch-integrator.js' ;
import { resolveTextualConflict } from './isolated-diff/textual-conflict-resolver.js' ;
import { runIsolatedWorkUnit } from './isolated-diff/work-unit-executor.js' ;
2026-05-10 23:12:26 +02:00
import { sanitizeMemoryFlowError } from './memory-flow/live-buffer.js' ;
2026-05-18 13:38:06 +02:00
import type { CanonicalPin } from './canonical-pins.js' ;
import type { MemoryFlowEvent , MemoryFlowEventSink , MemoryFlowPlannedWorkUnit } from './memory-flow/types.js' ;
import type {
ContextEvidenceIndexSummary ,
IngestBundleRunnerDeps ,
IngestProvenanceInsert ,
IngestProvenanceRow ,
IngestRunsPort ,
IngestSessionWorktree ,
PageTriageRunResult ,
} from './ports.js' ;
2026-05-10 23:12:26 +02:00
import { buildSyncId , rawSourcesDirForSync } from './raw-sources-paths.js' ;
import {
buildStageIndexFromReportBody ,
2026-05-20 14:17:10 +02:00
type IngestReportFinalizationProvenanceExclusion ,
type IngestReportFinalizationOutcome ,
2026-05-18 13:38:06 +02:00
type IngestReportProvenanceDetail ,
2026-05-10 23:12:26 +02:00
type IngestReportSnapshot ,
2026-05-18 13:38:06 +02:00
type IngestReportWorkUnit ,
2026-05-10 23:12:26 +02:00
} from './reports.js' ;
import {
buildReconcileSystemPrompt ,
buildReconcileToolSet ,
buildReconcileUserPrompt ,
} from './stages/build-reconcile-context.js' ;
import { buildWuSystemPrompt , buildWuToolSet , buildWuUserPrompt } from './stages/build-wu-context.js' ;
import { stageRawFilesStage1 } from './stages/stage-1-stage-raw-files.js' ;
import { executeWorkUnit , type WorkUnitOutcome } from './stages/stage-3-work-units.js' ;
import { runReconciliationStage4 } from './stages/stage-4-reconciliation.js' ;
import type { StageIndex } from './stages/stage-index.types.js' ;
import { validateWuTouchedSources } from './stages/validate-wu-sources.js' ;
2026-05-18 13:38:06 +02:00
import { assertSemanticLayerTargetPathsAllowed } from './semantic-layer-target-policy.js' ;
2026-05-10 23:12:26 +02:00
import { createEmitArtifactResolutionTool } from './tools/emit-artifact-resolution.tool.js' ;
import { createEmitConflictResolutionTool } from './tools/emit-conflict-resolution.tool.js' ;
import { createEmitEvictionDecisionTool } from './tools/emit-eviction-decision.tool.js' ;
import { createEmitUnmappedFallbackTool } from './tools/emit-unmapped-fallback.tool.js' ;
import { createEvictionListTool } from './tools/eviction-list.tool.js' ;
import { createReadRawSpanTool } from './tools/read-raw-span.tool.js' ;
import { createStageDiffTool } from './tools/stage-diff.tool.js' ;
import { createStageListTool } from './tools/stage-list.tool.js' ;
import { type ToolCallLogEntry , wrapToolsWithLogger } from './tools/tool-call-logger.js' ;
2026-05-12 16:56:58 -04:00
import {
createMutableToolTranscriptSummary ,
recordToolTranscriptEntry ,
type MutableToolTranscriptSummary ,
} from './tools/tool-transcript-summary.js' ;
2026-05-10 23:12:26 +02:00
import type {
2026-05-18 13:38:06 +02:00
IngestDiffSummary ,
2026-05-10 23:12:26 +02:00
EvictionUnit ,
IngestBundleJob ,
IngestBundleResult ,
IngestJobContext ,
UnresolvedCardInfo ,
WorkUnit ,
} from './types.js' ;
2026-05-13 13:43:23 +02:00
import { repairWikiSlRefs , type WikiSlRefRepairResult } from './wiki-sl-ref-repair.js' ;
2026-05-10 23:12:26 +02:00
2026-05-18 13:38:06 +02:00
type MemoryFlowStageProgress = Extract < MemoryFlowEvent , { type : 'stage_progress' } > ;
async function copyTransientIngestEvidence ( sourceWorkdir : string , targetWorkdir : string ) : Promise < void > {
const source = join ( sourceWorkdir , '.ktx/ingest-evidence' ) ;
const target = join ( targetWorkdir , '.ktx/ingest-evidence' ) ;
await cp ( source , target , { recursive : true , force : true } ) . catch ( ( error : NodeJS.ErrnoException ) = > {
if ( error . code !== 'ENOENT' ) {
throw error ;
}
} ) ;
}
2026-05-10 23:12:26 +02:00
function workUnitToMemoryFlowPlannedWorkUnit ( workUnit : WorkUnit ) : MemoryFlowPlannedWorkUnit {
return {
unitKey : workUnit.unitKey ,
rawFiles : workUnit.rawFiles ,
peerFileCount : workUnit.peerFileIndex.length ,
dependencyCount : workUnit.dependencyPaths.length ,
} ;
}
function stageIndexWorkUnitToMemoryFlowPlannedWorkUnit (
workUnit : StageIndex [ 'workUnits' ] [ number ] ,
) : MemoryFlowPlannedWorkUnit {
return {
unitKey : workUnit.unitKey ,
rawFiles : workUnit.rawFiles ,
peerFileCount : 0 ,
dependencyCount : 0 ,
} ;
}
function countMemoryFlowActions ( actions : MemoryAction [ ] , target : MemoryAction [ 'target' ] ) : number {
return actions . filter ( ( action ) = > action . target === target ) . length ;
}
2026-05-12 16:56:58 -04:00
function reportIdFromCreateResult ( result : unknown ) : string | undefined {
if ( ! result || typeof result !== 'object' || ! ( 'id' in result ) ) {
return undefined ;
2026-05-10 23:13:17 -07:00
}
2026-05-12 16:56:58 -04:00
const id = ( result as { id? : unknown } ) . id ;
return typeof id === 'string' && id . length > 0 ? id : undefined ;
}
function normalizeTableReference ( value : string ) : string {
return value
. trim ( )
. replace ( /["`]/g , '' )
. replace ( /[\[\]]/g , '' )
. toLowerCase ( ) ;
}
function finalReferenceSegment ( value : string ) : string {
const parts = value . split ( '.' ) . filter ( ( part ) = > part . length > 0 ) ;
return parts . at ( - 1 ) ? ? value ;
2026-05-10 23:13:17 -07:00
}
2026-05-12 16:56:58 -04:00
function semanticSourceMatchesTableRef ( source : SemanticLayerSource , tableRef : string ) : boolean {
const normalizedRef = normalizeTableReference ( tableRef ) ;
if ( ! normalizedRef ) {
return false ;
}
const refIsQualified = normalizedRef . includes ( '.' ) ;
const normalizedSourceName = normalizeTableReference ( source . name ) ;
if ( normalizedSourceName === normalizedRef ) {
2026-05-10 23:13:17 -07:00
return true ;
}
2026-05-12 16:56:58 -04:00
const table = typeof source . table === 'string' ? normalizeTableReference ( source . table ) : '' ;
if ( table && ( table === normalizedRef || table . endsWith ( ` . ${ normalizedRef } ` ) ) ) {
return true ;
2026-05-10 23:12:26 +02:00
}
2026-05-12 16:56:58 -04:00
if ( ! refIsQualified && table && finalReferenceSegment ( table ) === normalizedRef ) {
return true ;
}
return false ;
}
function rawPathsForAction ( action : MemoryAction , fallbackRawPaths : string [ ] ) : string [ ] {
return action . rawPaths && action . rawPaths . length > 0 ? [ . . . new Set ( action . rawPaths ) ] : fallbackRawPaths ;
2026-05-10 23:12:26 +02:00
}
2026-05-18 13:38:06 +02:00
type ProvenanceRowOrigin =
| {
source : 'work_unit_action' ;
unitKey : string ;
unitIndex : number ;
unitRawFiles : string [ ] ;
actionIndex : number ;
action : MemoryAction ;
}
| {
source : 'reconciliation_action' ;
actionIndex : number ;
action : MemoryAction ;
}
2026-05-20 14:17:10 +02:00
| {
source : 'finalization_action' ;
actionIndex : number ;
action : MemoryAction ;
}
2026-05-18 13:38:06 +02:00
| {
source : 'artifact_resolution' ;
resolutionIndex : number ;
resolution : NonNullable < StageIndex [ 'artifactResolutions' ] > [ number ] ;
}
| {
source : 'raw_snapshot_fallback' ;
rawPath : string ;
} ;
interface ProvenanceRowDiagnostic {
row : IngestProvenanceInsert ;
origin : ProvenanceRowOrigin ;
}
interface ProvenancePlan {
rows : IngestProvenanceInsert [ ] ;
diagnostics : ProvenanceRowDiagnostic [ ] ;
}
2026-05-10 23:12:26 +02:00
export class IngestBundleRunner {
2026-05-10 23:51:24 +02:00
private readonly logger : KtxLogger ;
2026-05-10 23:12:26 +02:00
private readonly chainByConnection = new Map < string , Promise < unknown > > ( ) ;
constructor ( private readonly deps : IngestBundleRunnerDeps ) {
this . logger = deps . logger ? ? noopLogger ;
}
async run ( job : IngestBundleJob , ctx? : IngestJobContext ) : Promise < IngestBundleResult > {
const key = job . connectionId ;
const previous = this . chainByConnection . get ( key ) ;
if ( previous ) {
this . logger . log ( ` [ingest-bundle] queued behind previous job for connection= ${ key } ` ) ;
}
const run = ( previous ? ? Promise . resolve ( ) ) . catch ( ( ) = > undefined ) . then ( ( ) = > this . runInner ( job , ctx ) ) ;
const chainSlot = run . finally ( ( ) = > {
if ( this . chainByConnection . get ( key ) === chainSlot ) {
this . chainByConnection . delete ( key ) ;
}
} ) ;
// Keep the chain alive but silence unhandled rejection — callers await `run` directly.
chainSlot . catch ( ( ) = > undefined ) ;
this . chainByConnection . set ( key , chainSlot ) ;
try {
const result = await run ;
ctx ? . memoryFlow ? . finish ( 'done' ) ;
return { . . . result , jobId : job.jobId } ;
} catch ( error ) {
ctx ? . memoryFlow ? . finish ( 'error' , [ sanitizeMemoryFlowError ( error ) ] ) ;
throw error ;
}
}
protected stageRawFilesStage1 = stageRawFilesStage1 ;
private async syncKnowledgeSlRefsFromActions ( connectionId : string , actions : MemoryAction [ ] ) : Promise < void > {
if ( ! this . deps . knowledgeSlRefs ) {
return ;
}
const slTargetsBySourceName = new Map < string , Set < string > > ( ) ;
const wikiActionsByKey = new Map < string , MemoryAction > ( ) ;
for ( const action of actions ) {
if ( action . target === 'sl' ) {
const bucket = slTargetsBySourceName . get ( action . key ) ? ? new Set < string > ( ) ;
bucket . add ( actionTargetConnectionId ( action , connectionId ) ) ;
slTargetsBySourceName . set ( action . key , bucket ) ;
}
if ( action . target === 'wiki' ) {
wikiActionsByKey . set ( action . key , action ) ;
}
}
for ( const action of wikiActionsByKey . values ( ) ) {
if ( action . type === 'removed' ) {
await this . deps . knowledgeSlRefs . syncFromWiki ( {
wikiPageKey : action.key ,
wikiScope : 'GLOBAL' ,
wikiScopeId : null ,
refs : [ ] ,
} ) ;
continue ;
}
const page = await this . deps . wikiService . readPage ( 'GLOBAL' , null , action . key ) ;
const bareSources = [
. . . new Set (
( page ? . frontmatter . sl_refs ? ? [ ] )
. map ( ( ref ) = > ref . split ( '.' ) [ 0 ] )
. filter ( ( sourceName ) : sourceName is string = > sourceName . length > 0 ) ,
) ,
] ;
const refs = bareSources . flatMap ( ( sourceName ) = > {
const targets = slTargetsBySourceName . get ( sourceName ) ;
if ( ! targets || targets . size === 0 ) {
return [ { connectionId , sourceName } ] ;
}
return [ . . . targets ] . sort ( ) . map ( ( targetConnectionId ) = > ( { connectionId : targetConnectionId , sourceName } ) ) ;
} ) ;
await this . deps . knowledgeSlRefs . syncFromWiki ( {
wikiPageKey : action.key ,
wikiScope : 'GLOBAL' ,
wikiScopeId : null ,
refs ,
} ) ;
}
}
protected async materializeOverrideSnapshot (
report : IngestReportSnapshot ,
ctx : { connectionId : string ; sourceKey : string ; jobId : string } ,
) : Promise < string > {
const rawRoot = rawSourcesDirForSync ( ctx . connectionId , ctx . sourceKey , report . body . syncId ) ;
const files = await this . deps . gitService . listFilesAtHead ( rawRoot ) ;
if ( files . length === 0 ) {
throw new Error ( ` override ingest: no raw-source files found for prior sync ${ report . body . syncId } ` ) ;
}
const stagedDir = this . deps . storage . resolvePullDir ( ctx . jobId ) ;
await rm ( stagedDir , { recursive : true , force : true } ) ;
await mkdir ( stagedDir , { recursive : true } ) ;
for ( const file of files ) {
const relativePath = file . startsWith ( ` ${ rawRoot } / ` ) ? file . slice ( rawRoot . length + 1 ) : file ;
const absoluteTarget = join ( stagedDir , relativePath ) ;
await mkdir ( dirname ( absoluteTarget ) , { recursive : true } ) ;
await writeFile ( absoluteTarget , await this . deps . gitService . getFileAtCommit ( file , 'HEAD' ) , 'utf-8' ) ;
}
return stagedDir ;
}
protected async loadOverrideReport ( job : IngestBundleJob ) : Promise < IngestReportSnapshot | null > {
if ( job . bundleRef . kind !== 'override' ) {
return null ;
}
const report = await this . deps . reports . findByJobId ( job . bundleRef . priorJobId ) ;
if ( ! report ) {
throw new Error ( ` override ingest: prior report ${ job . bundleRef . priorJobId } not found ` ) ;
}
if ( report . connectionId !== job . connectionId || report . sourceKey !== job . sourceKey ) {
throw new Error (
` override ingest: prior report ${ job . bundleRef . priorJobId } belongs to ${ report . connectionId } / ${ report . sourceKey } , not ${ job . connectionId } / ${ job . sourceKey } ` ,
) ;
}
return report ;
}
protected async resolveStagedDir (
ref : IngestBundleJob [ 'bundleRef' ] ,
2026-05-18 13:38:06 +02:00
ctx : { connectionId : string ; sourceKey : string ; jobId : string ; memoryFlow? : MemoryFlowEventSink } ,
2026-05-10 23:12:26 +02:00
) : Promise < string > {
if ( ref . kind === 'upload' ) {
return this . deps . storage . resolveUploadDir ( ref . uploadId ) ;
}
if ( ref . kind === 'override' ) {
throw new Error ( 'override bundle refs must be materialized from the prior report snapshot' ) ;
}
const stagedDir = this . deps . storage . resolvePullDir ( ctx . jobId ) ;
await mkdir ( stagedDir , { recursive : true } ) ;
const adapter = this . deps . registry . get ( ctx . sourceKey ) ;
if ( ! adapter . fetch ) {
throw new Error ( ` source adapter ' ${ ctx . sourceKey } ' does not support scheduled_pull (no fetch() method) ` ) ;
}
2026-05-18 13:38:06 +02:00
await adapter . fetch ( ref . config , stagedDir , {
connectionId : ctx.connectionId ,
sourceKey : ctx.sourceKey ,
. . . ( ctx . memoryFlow ? { memoryFlow : ctx.memoryFlow } : { } ) ,
} ) ;
2026-05-10 23:12:26 +02:00
return stagedDir ;
}
protected buildCommitMessage (
job : IngestBundleJob ,
syncId : string ,
diffSummary : { added : number ; modified : number ; deleted : number ; unchanged : number } ,
failedWUs : string [ ] ,
) : string {
const diff = ` + ${ diffSummary . added } /~ ${ diffSummary . modified } /- ${ diffSummary . deleted } /= ${ diffSummary . unchanged } ` ;
const failed = failedWUs . length > 0 ? ` ; failed WUs: ${ failedWUs . join ( ', ' ) } ` : '' ;
return ` ingest( ${ job . sourceKey } ): ${ job . jobId } syncId= ${ syncId } diff= ${ diff } ${ failed } ` ;
}
private async buildWikiIndex ( ) : Promise < string > {
const pages = await this . deps . knowledgeIndex ? . listPagesForUser ( 'system' ) ;
if ( ! pages || pages . length === 0 ) {
return '(empty)' ;
}
2026-05-13 16:05:58 +02:00
return ` ## Wiki Pages \ n ${ pages . map ( ( page ) = > ` - ${ page . page_key } : ${ page . summary } ` ) . join ( '\n' ) } ` ;
2026-05-10 23:12:26 +02:00
}
private async buildSlIndex ( connectionIds : string [ ] ) : Promise < string > {
const blocks = await Promise . all (
connectionIds . map ( async ( connectionId ) = > {
try {
fix(context): merge overlay columns onto manifest columns by name (#94)
* fix(context): merge overlay columns onto manifest columns by name
composeOverlay was appending overlay columns to the manifest column list,
producing duplicate entries when dbt/metabase overlays declared a column
just to attach descriptions. The duplicates carried no `type`, so the
pydantic SourceDefinition rejected them at semantic-query time and broke
`ktx sl query` for every overlay-backed measure. Now overlay columns
match base columns by name (case-insensitive): same-name entries merge
onto the manifest (overlay fields win, type/role fall back to the base,
descriptions merge per source key) and only new names append.
* refactor(sl): split overlay columns from column_overrides and enforce TS/Python wire contract
Overlay sources now have two distinct collections: `columns:` for computed
columns (requiring `expr` + `type`) and `column_overrides:` for metadata
patches to inherited manifest columns. Composing or loading an overlay that
mixes the two — or references an unknown column — fails with a typed error.
Introduce `ResolvedSemanticLayerSource` / `resolvedSourceSchema` /
`toResolvedWire` as the strict shape sent to the Python engine, and add a
schema contract test that diffs Zod against the Pydantic JSON schema dumped
by `python -m semantic_layer dump-schema`. `SourceDefinition` is now
`extra="forbid"` on the Python side.
`loadAllSources` surfaces per-file load errors instead of swallowing them,
so validation/query paths can report manifest shard parse failures.
* fix(context): make scan description generation resilient and quiet
A transient sampleTable failure during ingest used to take out every
table in a connection: generateTableDescription returned a hardcoded
'Table not found' string into descriptions.ai, and KtxDescriptionGenerator
was constructed without a logger, so the failure left no trail anywhere.
- sampleTable / sampleColumn calls retry 3x with 200/400/800ms backoff,
honouring KtxScanContext.signal via a new KtxAbortedError.
- On retry exhaustion or missing capability, table generation falls back
to a metadata-only prompt built from column name / native type / comment
/ rawDescriptions. The column path follows the same rule -- call the
LLM when any of samples or rawDescriptions are available; skip only
when both are absent.
- Logger is now threaded from KtxScanContext into the generator. Failures
emit structured KtxScanWarning entries (new description_fallback_used
code, plus existing sampling_failed / enrichment_failed /
connector_capability_missing). ktx scan groups warnings by code so a
batch of identical failures collapses to one summary line plus sample.
- Returns null on failure instead of the 'Table not found' sentinel; the
manifest writer's existing guard already skips empty descriptions, so
schema YAML no longer carries misleading text. SCAN_MANAGED_DESCRIPTION_KEYS
already strips stale 'ai' on merge, so existing YAML clears on next run.
Also suppress AI SDK v6 'system in messages' warning: pull system messages
out of KtxMessageBuilder.wrapSimple's output via a new splitKtxSystemMessages
helper and pass them top-level to generateText (preserves cacheControl
providerOptions on the SystemModelMessage). Agent-runner's local
splitSystemPromptMessages dedupes onto the shared helper.
* test(docs): align examples-docs assertions with revamped docs
PR #103 (setup/guide doc revamp) reworded several CLI examples and
connection labels; the assertions in scripts/examples-docs.test.mjs
still referenced the pre-revamp wording and were failing in CI on main.
Update the regexes to match the post-revamp content:
- drop the `--json` flag from the sl-query example expectation
- move the `Driver:` / `Status: ok` probe to the connection reference,
which is where that output now lives (driver id is lowercase
`postgres`, not the display name `PostgreSQL`)
- drop the obsolete `Install \`uv\`...` troubleshooting line
- accept `<connectionId>` everywhere; the docs no longer use the
hyphenated `<connection-id>` form
- match the `warehouse` connection id used in the quickstart instead of
the `postgres-warehouse` id only used in the README and setup ref
* fix(sl): skip TS/Python schema contract test when uv is unavailable
The TypeScript checks CI job does not install uv or Python, so the
module-level `execFileSync('uv', ...)` in schemas.contract.test.ts threw
ENOENT and failed the suite. Wrap the schema dump in a try/catch and
guard the describe block with `describe.skipIf` so the test skips in
environments without uv. Local dev and any CI job that has uv on PATH
still runs the cross-language contract assertion.
2026-05-15 02:11:04 +02:00
const { sources } = await this . deps . semanticLayerService . loadAllSources ( connectionId ) ;
2026-05-12 16:56:58 -04:00
const names = sources . map ( ( source ) = > source . name ) . sort ( ( left , right ) = > left . localeCompare ( right ) ) ;
2026-05-10 23:12:26 +02:00
const body = names . length > 0 ? names . join ( '\n' ) : '(no sources yet)' ;
return ` ## ${ connectionId } \ n ${ body } ` ;
} catch {
2026-05-12 16:56:58 -04:00
try {
const files = await this . deps . semanticLayerService . listFilesForConnection ( connectionId ) ;
const names = files
. filter ( ( f ) = > ! f . startsWith ( '_schema/' ) )
. map ( ( f ) = > f . replace ( /\.yaml$/ , '' ) )
. sort ( ( left , right ) = > left . localeCompare ( right ) ) ;
const body = names . length > 0 ? names . join ( '\n' ) : '(no sources yet)' ;
return ` ## ${ connectionId } \ n ${ body } ` ;
} catch {
return ` ## ${ connectionId } \ n(empty) ` ;
}
2026-05-10 23:12:26 +02:00
}
} ) ,
) ;
return blocks . join ( '\n\n' ) ;
}
2026-05-12 16:56:58 -04:00
private async tableRefExistsInSemanticLayer (
semanticLayerService : SemanticLayerService ,
connectionIds : string [ ] ,
tableRef : string ,
) : Promise < boolean > {
for ( const connectionId of connectionIds ) {
try {
fix(context): merge overlay columns onto manifest columns by name (#94)
* fix(context): merge overlay columns onto manifest columns by name
composeOverlay was appending overlay columns to the manifest column list,
producing duplicate entries when dbt/metabase overlays declared a column
just to attach descriptions. The duplicates carried no `type`, so the
pydantic SourceDefinition rejected them at semantic-query time and broke
`ktx sl query` for every overlay-backed measure. Now overlay columns
match base columns by name (case-insensitive): same-name entries merge
onto the manifest (overlay fields win, type/role fall back to the base,
descriptions merge per source key) and only new names append.
* refactor(sl): split overlay columns from column_overrides and enforce TS/Python wire contract
Overlay sources now have two distinct collections: `columns:` for computed
columns (requiring `expr` + `type`) and `column_overrides:` for metadata
patches to inherited manifest columns. Composing or loading an overlay that
mixes the two — or references an unknown column — fails with a typed error.
Introduce `ResolvedSemanticLayerSource` / `resolvedSourceSchema` /
`toResolvedWire` as the strict shape sent to the Python engine, and add a
schema contract test that diffs Zod against the Pydantic JSON schema dumped
by `python -m semantic_layer dump-schema`. `SourceDefinition` is now
`extra="forbid"` on the Python side.
`loadAllSources` surfaces per-file load errors instead of swallowing them,
so validation/query paths can report manifest shard parse failures.
* fix(context): make scan description generation resilient and quiet
A transient sampleTable failure during ingest used to take out every
table in a connection: generateTableDescription returned a hardcoded
'Table not found' string into descriptions.ai, and KtxDescriptionGenerator
was constructed without a logger, so the failure left no trail anywhere.
- sampleTable / sampleColumn calls retry 3x with 200/400/800ms backoff,
honouring KtxScanContext.signal via a new KtxAbortedError.
- On retry exhaustion or missing capability, table generation falls back
to a metadata-only prompt built from column name / native type / comment
/ rawDescriptions. The column path follows the same rule -- call the
LLM when any of samples or rawDescriptions are available; skip only
when both are absent.
- Logger is now threaded from KtxScanContext into the generator. Failures
emit structured KtxScanWarning entries (new description_fallback_used
code, plus existing sampling_failed / enrichment_failed /
connector_capability_missing). ktx scan groups warnings by code so a
batch of identical failures collapses to one summary line plus sample.
- Returns null on failure instead of the 'Table not found' sentinel; the
manifest writer's existing guard already skips empty descriptions, so
schema YAML no longer carries misleading text. SCAN_MANAGED_DESCRIPTION_KEYS
already strips stale 'ai' on merge, so existing YAML clears on next run.
Also suppress AI SDK v6 'system in messages' warning: pull system messages
out of KtxMessageBuilder.wrapSimple's output via a new splitKtxSystemMessages
helper and pass them top-level to generateText (preserves cacheControl
providerOptions on the SystemModelMessage). Agent-runner's local
splitSystemPromptMessages dedupes onto the shared helper.
* test(docs): align examples-docs assertions with revamped docs
PR #103 (setup/guide doc revamp) reworded several CLI examples and
connection labels; the assertions in scripts/examples-docs.test.mjs
still referenced the pre-revamp wording and were failing in CI on main.
Update the regexes to match the post-revamp content:
- drop the `--json` flag from the sl-query example expectation
- move the `Driver:` / `Status: ok` probe to the connection reference,
which is where that output now lives (driver id is lowercase
`postgres`, not the display name `PostgreSQL`)
- drop the obsolete `Install \`uv\`...` troubleshooting line
- accept `<connectionId>` everywhere; the docs no longer use the
hyphenated `<connection-id>` form
- match the `warehouse` connection id used in the quickstart instead of
the `postgres-warehouse` id only used in the README and setup ref
* fix(sl): skip TS/Python schema contract test when uv is unavailable
The TypeScript checks CI job does not install uv or Python, so the
module-level `execFileSync('uv', ...)` in schemas.contract.test.ts threw
ENOENT and failed the suite. Wrap the schema dump in a try/catch and
guard the describe block with `describe.skipIf` so the test skips in
environments without uv. Local dev and any CI job that has uv on PATH
still runs the cross-language contract assertion.
2026-05-15 02:11:04 +02:00
const { sources } = await semanticLayerService . loadAllSources ( connectionId ) ;
2026-05-12 16:56:58 -04:00
if ( sources . some ( ( source ) = > semanticSourceMatchesTableRef ( source , tableRef ) ) ) {
return true ;
}
} catch {
// Fallback diagnostics should not fail an ingest stage if an index lookup is temporarily unavailable.
}
}
return false ;
}
2026-05-20 14:17:10 +02:00
private async loadSourcesByConnection (
workdir : string ,
connectionIds : string [ ] ,
) : Promise < Map < string , SemanticLayerSource [ ] > > {
const service = this . deps . semanticLayerService . forWorktree ( workdir ) ;
const result = new Map < string , SemanticLayerSource [ ] > ( ) ;
for ( const connectionId of connectionIds ) {
const { sources } = await service . loadAllSources ( connectionId ) ;
result . set ( connectionId , sources ) ;
}
return result ;
}
2026-05-10 23:12:26 +02:00
private resolveContextCuratorBudget (
bundleRef : IngestBundleJob [ 'bundleRef' ] ,
stageIndex : StageIndex ,
) : { creates : number ; updates : number } {
const rawConfig =
bundleRef . kind === 'scheduled_pull' && bundleRef . config && typeof bundleRef . config === 'object'
? ( bundleRef . config as Record < string , unknown > )
: { } ;
const configuredCreates =
2026-05-12 16:56:58 -04:00
typeof rawConfig . maxKnowledgeCreatesPerRun === 'number'
? rawConfig . maxKnowledgeCreatesPerRun
: NOTION_DEFAULT_MAX_KNOWLEDGE_CREATES_PER_RUN ;
2026-05-10 23:12:26 +02:00
const configuredUpdates =
typeof rawConfig . maxKnowledgeUpdatesPerRun === 'number' ? rawConfig.maxKnowledgeUpdatesPerRun : 20 ;
const wikiActions = stageIndex . workUnits . flatMap ( ( wu ) = > wu . actions ) . filter ( ( action ) = > action . target === 'wiki' ) ;
const usedCreates = wikiActions . filter ( ( action ) = > action . type === 'created' ) . length ;
const usedUpdates = wikiActions . filter ( ( action ) = > action . type === 'updated' ) . length ;
return {
creates : Math.max ( 0 , configuredCreates - usedCreates ) ,
updates : Math.max ( 0 , configuredUpdates - usedUpdates ) ,
} ;
}
2026-05-18 13:38:06 +02:00
private filterWorkUnitsForTriage (
workUnits : WorkUnit [ ] ,
triageResult : { enabled : boolean ; fullRawPaths : Set < string > } | null ,
) : WorkUnit [ ] {
if ( ! triageResult ? . enabled ) {
return workUnits ;
}
return workUnits . filter ( ( wu ) = > wu . rawFiles . some ( ( rawPath ) = > triageResult . fullRawPaths . has ( rawPath ) ) ) ;
}
private createTrace ( job : IngestBundleJob ) : IngestTraceWriter {
const storage = this . deps . storage as typeof this . deps . storage & { resolveTracePath ? : ( jobId : string ) = > string } ;
return new FileIngestTraceWriter ( {
tracePath : storage.resolveTracePath?. ( job . jobId ) ? ? ingestTracePathForJob ( this . deps . storage . homeDir , job . jobId ) ,
jobId : job.jobId ,
connectionId : job.connectionId ,
sourceKey : job.sourceKey ,
level : this.deps.settings.ingestTraceLevel ? ? 'debug' ,
} ) ;
}
private errorMessage ( error : unknown ) : string {
return error instanceof Error ? error.message : String ( error ) ;
}
private buildProvenancePlan ( input : {
job : IngestBundleJob ;
syncId : string ;
currentHashes : Map < string , string > ;
stageIndex : StageIndex ;
reconcileActions : MemoryAction [ ] ;
2026-05-20 14:17:10 +02:00
finalizationActions : MemoryAction [ ] ;
2026-05-18 13:38:06 +02:00
} ) : ProvenancePlan {
const rows : IngestProvenanceInsert [ ] = [ ] ;
const diagnostics : ProvenanceRowDiagnostic [ ] = [ ] ;
const actionToType = ( action : MemoryAction ) : IngestProvenanceInsert [ 'actionType' ] = > {
if ( action . target === 'wiki' ) {
return 'wiki_written' ;
}
return action . type === 'created' ? 'source_created' : 'measure_added' ;
} ;
const producedPaths = new Set < string > ( ) ;
const pushRow = ( row : IngestProvenanceInsert , origin : ProvenanceRowOrigin ) : void = > {
rows . push ( row ) ;
diagnostics . push ( { row , origin } ) ;
producedPaths . add ( row . rawPath ) ;
} ;
const pushActionProvenance = ( rawPath : string , action : MemoryAction , origin : ProvenanceRowOrigin ) : void = > {
const hash = input . currentHashes . get ( rawPath ) ? ? '' ;
pushRow (
{
connectionId : input.job.connectionId ,
sourceKey : input.job.sourceKey ,
syncId : input.syncId ,
rawPath ,
rawContentHash : hash ,
artifactKind : action.target ,
artifactKey : action.key ,
targetConnectionId : action.target === 'sl' ? actionTargetConnectionId ( action , input . job . connectionId ) : null ,
artifactContentHash : null ,
actionType : actionToType ( action ) ,
} ,
origin ,
) ;
} ;
input . stageIndex . workUnits . forEach ( ( wu , unitIndex ) = > {
wu . actions . forEach ( ( action , actionIndex ) = > {
for ( const rawPath of rawPathsForAction ( action , wu . rawFiles ) ) {
pushActionProvenance ( rawPath , action , {
source : 'work_unit_action' ,
unitKey : wu.unitKey ,
unitIndex ,
unitRawFiles : wu.rawFiles ,
actionIndex ,
action ,
} ) ;
}
} ) ;
} ) ;
input . reconcileActions . forEach ( ( action , actionIndex ) = > {
for ( const rawPath of action . rawPaths ? ? [ ] ) {
pushActionProvenance ( rawPath , action , {
source : 'reconciliation_action' ,
actionIndex ,
action ,
} ) ;
}
} ) ;
2026-05-20 14:17:10 +02:00
input . finalizationActions . forEach ( ( action , actionIndex ) = > {
for ( const rawPath of action . rawPaths ? ? [ ] ) {
pushActionProvenance ( rawPath , action , {
source : 'finalization_action' ,
actionIndex ,
action ,
} ) ;
}
} ) ;
2026-05-18 13:38:06 +02:00
( input . stageIndex . artifactResolutions ? ? [ ] ) . forEach ( ( resolution , resolutionIndex ) = > {
const hash = input . currentHashes . get ( resolution . rawPath ) ? ? '' ;
pushRow (
{
connectionId : input.job.connectionId ,
sourceKey : input.job.sourceKey ,
syncId : input.syncId ,
rawPath : resolution.rawPath ,
rawContentHash : hash ,
artifactKind : resolution.artifactKind ,
artifactKey : resolution.artifactKey ,
targetConnectionId : null ,
artifactContentHash : null ,
actionType : resolution.actionType ,
} ,
{
source : 'artifact_resolution' ,
resolutionIndex ,
resolution ,
} ,
) ;
} ) ;
for ( const [ rawPath , hash ] of input . currentHashes ) {
if ( producedPaths . has ( rawPath ) ) {
continue ;
}
pushRow (
{
connectionId : input.job.connectionId ,
sourceKey : input.job.sourceKey ,
syncId : input.syncId ,
rawPath ,
rawContentHash : hash ,
artifactKind : null ,
artifactKey : null ,
targetConnectionId : null ,
artifactContentHash : null ,
actionType : 'skipped' ,
} ,
{ source : 'raw_snapshot_fallback' , rawPath } ,
) ;
}
return { rows , diagnostics } ;
}
2026-05-20 14:17:10 +02:00
private partitionFinalizationActionsForProvenance ( input : {
actions : MemoryAction [ ] ;
currentRawPaths : Set < string > ;
currentEvictionRawPaths : Set < string > ;
overrideEvictionRawPaths : Set < string > ;
} ) : { actions : MemoryAction [ ] ; exclusions : IngestReportFinalizationProvenanceExclusion [ ] } {
const defensible = new Set ( [
. . . input . currentRawPaths ,
. . . input . currentEvictionRawPaths ,
. . . input . overrideEvictionRawPaths ,
] ) ;
const actions : MemoryAction [ ] = [ ] ;
const exclusions : IngestReportFinalizationProvenanceExclusion [ ] = [ ] ;
for ( const action of input . actions ) {
const rawPaths = action . rawPaths ? ? [ ] ;
if ( rawPaths . length === 0 ) {
exclusions . push ( { action , reason : 'missing_raw_paths' } ) ;
continue ;
}
const invalidRawPaths = rawPaths . filter ( ( rawPath ) = > ! defensible . has ( rawPath ) ) . sort ( ) ;
if ( invalidRawPaths . length > 0 ) {
exclusions . push ( { action , reason : 'raw_path_not_defensible' , invalidRawPaths } ) ;
continue ;
}
actions . push ( action ) ;
}
return { actions , exclusions } ;
}
2026-05-18 13:38:06 +02:00
private toReportProvenanceRows ( rows : IngestProvenanceInsert [ ] ) : IngestReportProvenanceDetail [ ] {
return rows . map ( ( { rawPath , artifactKind , artifactKey , actionType , targetConnectionId } ) = > ( {
rawPath ,
artifactKind ,
artifactKey ,
targetConnectionId : targetConnectionId ? ? null ,
actionType ,
} ) ) ;
}
private toReportWorkUnits ( stageIndex : StageIndex ) : IngestReportWorkUnit [ ] {
return stageIndex . workUnits . map ( ( wu ) = > ( {
2026-05-10 23:12:26 +02:00
unitKey : wu.unitKey ,
2026-05-18 13:38:06 +02:00
rawFiles : wu.rawFiles ,
status : wu.status ,
reason : wu.reason ,
actions : wu.actions ,
touchedSlSources : wu.touchedSlSources ,
2026-05-10 23:12:26 +02:00
slDisallowed : wu.slDisallowed ,
slDisallowedReason : wu.slDisallowedReason ,
2026-05-18 13:38:06 +02:00
} ) ) ;
}
private provenanceValidationTraceData ( input : {
plan : ProvenancePlan ;
currentRawPaths : Set < string > ;
deletedRawPaths : Set < string > ;
} ) : Record < string , unknown > {
const invalidRows = input . plan . diagnostics . filter (
( { row } ) = > ! input . currentRawPaths . has ( row . rawPath ) && ! input . deletedRawPaths . has ( row . rawPath ) ,
) ;
return {
rowCount : input.plan.rows.length ,
currentRawPathCount : input.currentRawPaths.size ,
deletedRawPathCount : input.deletedRawPaths.size ,
currentRawPaths : [ . . . input . currentRawPaths ] . sort ( ) ,
deletedRawPaths : [ . . . input . deletedRawPaths ] . sort ( ) ,
invalidRawPaths : [ . . . new Set ( invalidRows . map ( ( { row } ) = > row . rawPath ) ) ] . sort ( ) ,
invalidRows ,
2026-05-10 23:12:26 +02:00
} ;
}
2026-05-18 13:38:06 +02:00
private wikiPageKeysFromPaths ( paths : string [ ] ) : string [ ] {
return [
. . . new Set (
paths
. filter ( ( path ) = > path . startsWith ( 'wiki/global/' ) && path . endsWith ( '.md' ) )
. map ( ( path ) = > path . slice ( 'wiki/global/' . length , - '.md' . length ) ) ,
) ,
] . sort ( ) ;
2026-05-10 23:12:26 +02:00
}
2026-05-18 13:38:06 +02:00
private touchedSlSourcesFromPaths ( paths : string [ ] ) : TouchedSlSource [ ] {
return paths
. filter ( ( path ) = > path . startsWith ( 'semantic-layer/' ) && path . endsWith ( '.yaml' ) && ! path . includes ( '/_schema/' ) )
. map ( ( path ) = > {
const [ , connectionId , fileName ] = path . split ( '/' ) ;
return { connectionId : connectionId ? ? '' , sourceName : ( fileName ? ? '' ) . replace ( /\.yaml$/ , '' ) } ;
} )
. filter ( ( source ) = > source . connectionId . length > 0 && source . sourceName . length > 0 ) ;
}
private touchedSlSourcesFromActions ( actions : MemoryAction [ ] , fallbackConnectionId : string ) : TouchedSlSource [ ] {
return actions
. filter ( ( action ) = > action . target === 'sl' )
. map ( ( action ) = > ( {
connectionId : actionTargetConnectionId ( action , fallbackConnectionId ) ,
sourceName : action.key ,
} ) ) ;
}
private wikiPageKeysFromActions ( actions : MemoryAction [ ] ) : string [ ] {
return actions . filter ( ( action ) = > action . target === 'wiki' ) . map ( ( action ) = > action . key ) ;
}
private uniqueWikiPageKeys ( keys : string [ ] ) : string [ ] {
return [ . . . new Set ( keys . filter ( ( key ) : key is string = > typeof key === 'string' && key . length > 0 ) ) ] . sort ( ) ;
}
private uniqueTouchedSlSources ( sources : TouchedSlSource [ ] ) : TouchedSlSource [ ] {
const seen = new Set < string > ( ) ;
const unique : TouchedSlSource [ ] = [ ] ;
for ( const source of sources ) {
const key = ` ${ source . connectionId } : ${ source . sourceName } ` ;
if ( seen . has ( key ) ) {
continue ;
}
seen . add ( key ) ;
unique . push ( source ) ;
2026-05-10 23:12:26 +02:00
}
2026-05-18 13:38:06 +02:00
return unique . sort ( ( left , right ) = > {
const byConnection = left . connectionId . localeCompare ( right . connectionId ) ;
return byConnection === 0 ? left . sourceName . localeCompare ( right . sourceName ) : byConnection ;
} ) ;
}
private removedWikiPageKeysFromActions ( actions : MemoryAction [ ] ) : string [ ] {
return this . uniqueWikiPageKeys (
actions . filter ( ( action ) = > action . target === 'wiki' && action . type === 'removed' ) . map ( ( action ) = > action . key ) ,
) ;
}
private finalGateActionOrigins ( input : {
stageIndex : StageIndex ;
reconcileActions : MemoryAction [ ] ;
fallbackConnectionId : string ;
} ) {
const actionContext = ( action : MemoryAction , fallbackRawPaths : string [ ] ) = > ( {
target : action.target ,
type : action . type ,
key : action.key ,
detail : action.detail ,
rawPaths : rawPathsForAction ( action , fallbackRawPaths ) ,
. . . ( action . target === 'sl' ? { targetConnectionId : actionTargetConnectionId ( action , input . fallbackConnectionId ) } : { } ) ,
} ) ;
return [
. . . input . stageIndex . workUnits . flatMap ( ( workUnit , unitIndex ) = >
workUnit . actions . map ( ( action , actionIndex ) = > ( {
source : 'work_unit_action' ,
unitKey : workUnit.unitKey ,
unitIndex ,
unitRawFiles : workUnit.rawFiles ,
actionIndex ,
action : actionContext ( action , workUnit . rawFiles ) ,
} ) ) ,
) ,
. . . input . reconcileActions . map ( ( action , actionIndex ) = > ( {
source : 'reconciliation_action' ,
actionIndex ,
action : actionContext ( action , [ ] ) ,
} ) ) ,
] ;
}
private async wikiPageKeysForFinalGates ( input : {
wikiService : ReturnType < KnowledgeWikiService [ 'forWorktree' ] > ;
changedWikiPageKeys : string [ ] ;
touchedSlSources : TouchedSlSource [ ] ;
actions : MemoryAction [ ] ;
} ) : Promise < {
pageKeys : string [ ] ;
trace : {
global : boolean ;
reasons : string [ ] ;
changedWikiPageKeys : string [ ] ;
removedWikiPageKeys : string [ ] ;
pageKeysValidated : string [ ] ;
} ;
} > {
const changedWikiPageKeys = this . uniqueWikiPageKeys ( input . changedWikiPageKeys ) ;
const removedWikiPageKeys = this . removedWikiPageKeysFromActions ( input . actions ) ;
const reasons : string [ ] = [ ] ;
if ( input . touchedSlSources . length > 0 ) {
reasons . push ( 'semantic_layer_changed' ) ;
}
if ( removedWikiPageKeys . length > 0 ) {
reasons . push ( 'wiki_page_removed' ) ;
}
let pageKeys = changedWikiPageKeys ;
if ( reasons . length > 0 ) {
pageKeys = this . uniqueWikiPageKeys ( [
. . . changedWikiPageKeys ,
. . . ( await input . wikiService . listPageKeys ( 'GLOBAL' , null ) ) ,
] ) ;
}
return {
pageKeys ,
trace : {
global : reasons . length > 0 ,
reasons ,
changedWikiPageKeys ,
removedWikiPageKeys ,
pageKeysValidated : pageKeys ,
} ,
} ;
}
private async runWorkUnitInWorktree ( input : {
job : IngestBundleJob ;
syncId : string ;
wu : WorkUnit ;
worktree : IngestSessionWorktree ;
stagedDir : string ;
contextReport : ContextEvidenceIndexSummary | null ;
ingestToolMetadata : { runId : string ; jobId : string ; syncId : string ; sourceKey : string } ;
slConnectionIds : string [ ] ;
wikiIndex : string ;
slIndex : string ;
priorProvenance : Map < string , IngestProvenanceRow [ ] > ;
scopedWikiService : ReturnType < KnowledgeWikiService [ 'forWorktree' ] > ;
scopedSemanticLayerService : ReturnType < SemanticLayerService [ 'forWorktree' ] > ;
baseFraming : string ;
skillsPrompt : string ;
canonicalPins : CanonicalPin [ ] ;
workUnitSettings : { maxConcurrency : number ; stepBudget : number ; failureMode : 'abort' | 'continue' } ;
transcriptDir : string ;
transcriptSummaries : Map < string , MutableToolTranscriptSummary > ;
recordTranscriptEntry ( path : string ) : ( entry : ToolCallLogEntry ) = > void ;
stageIndex : StageIndex ;
includeContextEvidenceTools : boolean ;
currentTableExists ( tableRef : string ) : Promise < boolean > ;
memoryFlow? : MemoryFlowEventSink ;
wuSkillNames : string [ ] ;
onStepFinish ? : ( info : { stepIndex : number ; stepBudget : number } ) = > void ;
} ) : Promise < WorkUnitOutcome > {
const session : CaptureSession = {
userId : 'system' ,
chatId : input.wu.unitKey ,
userMessage : ` ingest( ${ input . job . sourceKey } ) WU= ${ input . wu . unitKey } ` ,
connectionId : input.job.connectionId ,
userScopedEnabled : false ,
forceGlobalScope : true ,
touchedSlSources : createTouchedSlSources ( ) ,
preHead : input.worktree.baseSha ,
} ;
const sessionActions : MemoryAction [ ] = [ ] ;
const toolSession : ToolSession = {
connectionId : input.job.connectionId ,
isWorktreeScoped : true ,
preHead : input.worktree.baseSha ,
touchedSlSources : session.touchedSlSources ,
actions : sessionActions ,
allowedRawPaths : new Set ( input . wu . rawFiles ) ,
allowedConnectionNames : new Set ( input . slConnectionIds ) ,
semanticLayerService : input.scopedSemanticLayerService ,
wikiService : input.scopedWikiService ,
configService : input.worktree.config ,
gitService : input.worktree.git ,
ingest : input.ingestToolMetadata ,
} ;
const slValidationDeps : SlValidationDeps = {
semanticLayerService : input.scopedSemanticLayerService ,
connections : this.deps.connections ,
configService : input.worktree.config ,
gitService : input.worktree.git ,
slSourcesRepository : this.deps.slSourcesRepository ,
probeRowCount : this.deps.settings.probeRowCount ,
} ;
const wuToolset = this . deps . toolsetFactory . createIngestWuToolset ( toolSession , {
includeContextEvidenceTools : input.includeContextEvidenceTools ,
} ) ;
const wuToolContext : ToolContext = {
sourceId : 'ingest' ,
messageId : ` ${ input . job . jobId } -wu- ${ input . wu . unitKey } ` ,
userId : 'system' ,
connectionId : input.job.connectionId ,
ingest : input.ingestToolMetadata ,
session : toolSession ,
} ;
const skillsLoadedPerWu : string [ ] = [ ] ;
const loadSkillTool : KtxRuntimeToolSet = {
load_skill : {
name : 'load_skill' ,
description :
'Load a skill to get specialized instructions. Call this when a skill listed in the system prompt matches the current task.' ,
inputSchema : z.object ( { name : z.string ( ) } ) ,
execute : async ( { name } ) = > {
const skill = await this . deps . skillsRegistry . getSkill ( name , 'memory_agent' ) ;
if ( ! skill ) {
const available =
( await this . deps . skillsRegistry . listSkills ( 'memory_agent' ) ) . map ( ( s ) = > s . name ) . join ( ', ' ) || '(none)' ;
return { markdown : ` Skill " ${ name } " not available. Available: ${ available } ` } ;
}
const body = await readFile ( join ( skill . path , 'SKILL.md' ) , 'utf-8' ) ;
if ( ! skillsLoadedPerWu . includes ( skill . name ) ) {
skillsLoadedPerWu . push ( skill . name ) ;
}
const structured = {
name : skill.name ,
skillDirectory : skill.path ,
content : this.deps.skillsRegistry.stripFrontmatter ( body ) ,
} ;
return {
markdown : ` # ${ structured . name } \ n \ n ${ structured . content } ` ,
structured ,
} ;
} ,
} ,
} ;
const wuEmitUnmappedFallbackTool = {
emit_unmapped_fallback : createRuntimeToolDescriptorFromAiTool (
'emit_unmapped_fallback' ,
createEmitUnmappedFallbackTool ( {
stageIndex : input.stageIndex ,
allowedPaths : new Set ( input . wu . rawFiles ) ,
tableRefExists : input.currentTableExists ,
} ) ,
) ,
} ;
const systemPrompt = buildWuSystemPrompt ( {
baseFraming : input.baseFraming ,
skillsPrompt : input.skillsPrompt ,
syncId : input.syncId ,
sourceKey : input.job.sourceKey ,
canonicalPins : input.canonicalPins ,
} ) ;
input . memoryFlow ? . emit ( {
type : 'work_unit_started' ,
unitKey : input.wu.unitKey ,
skills : input.wuSkillNames ,
stepBudget : input.workUnitSettings.stepBudget ,
} ) ;
return executeWorkUnit (
{
sessionWorktreeGit : input.worktree.git ,
agentRunner : this.deps.agentRunner ,
validateTouchedSources : ( touched ) = >
validateWuTouchedSources ( { . . . slValidationDeps , slValidator : this.deps.slValidator } , touched ) ,
validateWikiRefs : ( actions ) = >
findDanglingWikiRefsForActions ( {
wikiService : input.scopedWikiService ,
scope : 'GLOBAL' ,
scopeId : null ,
actions ,
} ) ,
resetHardTo : ( targetSha ) = > input . worktree . git . resetHardTo ( targetSha ) ,
buildSystemPrompt : ( ) = > systemPrompt ,
buildUserPrompt : ( wuInner ) = >
buildWuUserPrompt ( {
wu : wuInner ,
wikiIndex : input.wikiIndex ,
slIndex : input.slIndex ,
priorProvenance : input.priorProvenance ,
} ) ,
buildToolSet : ( wuInner ) = >
wrapToolsWithLogger (
buildWuToolSet ( {
sourceKey : input.job.sourceKey ,
stagedDir : input.stagedDir ,
wu : wuInner ,
loadSkillTool ,
emitUnmappedFallbackTool : wuEmitUnmappedFallbackTool ,
toolsetTools : wuToolset.toRuntimeTools ( wuToolContext ) ,
} ) ,
join ( input . transcriptDir , ` ${ wuInner . unitKey } .jsonl ` ) ,
wuInner . unitKey ,
{ onEntry : input.recordTranscriptEntry ( join ( input . transcriptDir , ` ${ wuInner . unitKey } .jsonl ` ) ) } ,
) ,
captureSession : session ,
sessionActions ,
modelRole : 'candidateExtraction' ,
stepBudget : input.workUnitSettings.stepBudget ,
sourceKey : input.job.sourceKey ,
connectionId : input.job.connectionId ,
jobId : input.job.jobId ,
toolFailureCount : ( unitKey ) = > input . transcriptSummaries . get ( unitKey ) ? . fatalErrorCount ? ? 0 ,
onStepFinish : input.onStepFinish ,
} ,
input . wu ,
) ;
2026-05-10 23:12:26 +02:00
}
protected async runInner ( job : IngestBundleJob , ctx? : IngestJobContext ) : Promise < Omit < IngestBundleResult , 'jobId' > > {
const syncId = buildSyncId ( new Date ( ) , job . jobId ) ;
2026-05-18 13:38:06 +02:00
const trace = this . createTrace ( job ) ;
const transcriptSummaries = new Map < string , MutableToolTranscriptSummary > ( ) ;
let activeTrace : IngestTraceWriter = trace ;
let activePhase = 'run' ;
let runRow : Awaited < ReturnType < IngestRunsPort [ 'create' ] > > | null = null ;
let latestDiffSummary : IngestDiffSummary = { added : 0 , modified : 0 , deleted : 0 , unchanged : 0 } ;
let latestWorkUnits : WorkUnitOutcome [ ] = [ ] ;
let latestFailedWorkUnits : string [ ] = [ ] ;
let latestReconciliationSkipped = true ;
let latestReportWorkUnits : IngestReportWorkUnit [ ] = [ ] ;
let latestReconciliationActions : MemoryAction [ ] = [ ] ;
let latestConflictsResolved : StageIndex [ 'conflictsResolved' ] = [ ] ;
let latestEvictionsApplied : StageIndex [ 'evictionsApplied' ] = [ ] ;
let latestUnmappedFallbacks : StageIndex [ 'unmappedFallbacks' ] = [ ] ;
let latestArtifactResolutions : NonNullable < StageIndex [ 'artifactResolutions' ] > = [ ] ;
let latestEvictionInputs : string [ ] = [ ] ;
let latestUnresolvedCards : UnresolvedCardInfo [ ] = [ ] ;
let latestReportProvenanceRows : IngestReportProvenanceDetail [ ] = [ ] ;
2026-05-20 14:17:10 +02:00
let latestFinalizationOutcome : IngestReportFinalizationOutcome | undefined ;
2026-05-18 13:38:06 +02:00
let activeFailureDetails : Record < string , unknown > | undefined ;
let latestIsolatedDiffSummary :
| {
enabled : boolean ;
integrationWorktreePath? : string ;
ingestionBaseSha? : string ;
projectionSha? : string | null ;
acceptedPatches : number ;
textualConflicts : number ;
semanticConflicts : number ;
resolverAttempts : number ;
resolverRepairs : number ;
resolverFailures : number ;
}
| undefined ;
await trace . event ( 'info' , 'run' , 'ingest_started' , {
trigger : job.trigger ,
bundleRefKind : job.bundleRef.kind ,
} ) ;
try {
2026-05-10 23:12:26 +02:00
const memoryFlow = ctx ? . memoryFlow ;
2026-05-18 13:38:06 +02:00
const emitStageProgress = (
stage : MemoryFlowStageProgress [ 'stage' ] ,
percent : number ,
message : string ,
options : { transient? : boolean } = { } ,
) : void = > {
memoryFlow ? . emit ( {
type : 'stage_progress' ,
stage ,
percent ,
message ,
. . . ( options . transient !== undefined ? { transient : options.transient } : { } ) ,
} ) ;
} ;
2026-05-10 23:12:26 +02:00
const baseSha = await this . deps . lockingService . withLock ( 'config:repo' , ( ) = > this . deps . gitService . revParseHead ( ) ) ;
if ( ! baseSha ) {
throw new Error ( 'ingest-bundle: config repo has no HEAD' ) ;
}
const transcriptDir = this . deps . storage . resolveTranscriptDir ( job . jobId ) ;
const recordTranscriptEntry =
( path : string ) = >
( entry : ToolCallLogEntry ) : void = > {
const current =
2026-05-12 16:56:58 -04:00
transcriptSummaries . get ( entry . wuKey ) ? ? createMutableToolTranscriptSummary ( entry . wuKey , path ) ;
recordToolTranscriptEntry ( current , entry ) ;
2026-05-10 23:12:26 +02:00
transcriptSummaries . set ( entry . wuKey , current ) ;
} ;
const overrideReport = await this . loadOverrideReport ( job ) ;
const stage1 = ctx ? . startPhase ( 0.08 ) ;
await stage1 ? . updateProgress ( 0.0 , 'Fetching source files' ) ;
const adapter = this . deps . registry . get ( job . sourceKey ) ;
2026-05-18 13:38:06 +02:00
activePhase = 'fetch' ;
const stagedDir = await traceTimed (
trace ,
'fetch' ,
'resolve_staged_dir' ,
{
bundleRefKind : job.bundleRef.kind ,
sourceKey : job.sourceKey ,
} ,
( ) = >
overrideReport
? this . materializeOverrideSnapshot ( overrideReport , {
connectionId : job.connectionId ,
sourceKey : job.sourceKey ,
jobId : job.jobId ,
} )
: this . resolveStagedDir ( job . bundleRef , {
connectionId : job.connectionId ,
sourceKey : job.sourceKey ,
jobId : job.jobId ,
. . . ( memoryFlow ? { memoryFlow } : { } ) ,
} ) ,
) ;
2026-05-10 23:12:26 +02:00
const fetchReport = adapter . readFetchReport ? await adapter . readFetchReport ( stagedDir ) : null ;
const scopeDescriptor = adapter . describeScope ? await adapter . describeScope ( stagedDir ) : null ;
const sessionWorktree = await this . deps . lockingService . withLock ( 'config:repo' , ( ) = >
this . deps . sessionWorktreeService . create ( job . jobId , baseSha ) ,
) ;
2026-05-18 13:38:06 +02:00
let cleanupOutcome : 'success' | 'crash' | 'conflict' = 'crash' ;
2026-05-10 23:12:26 +02:00
try {
2026-05-18 13:38:06 +02:00
activePhase = 'stage_raw_files' ;
const { currentHashes , rawDirInWorktree } = await traceTimed (
trace ,
'stage_raw_files' ,
'stage_raw_files' ,
{
stagedDir ,
worktreePath : sessionWorktree.workdir ,
connectionId : job.connectionId ,
sourceKey : job.sourceKey ,
syncId ,
} ,
( ) = >
this . stageRawFilesStage1 ( {
stagedDir ,
worktreeRoot : sessionWorktree.workdir ,
connectionId : job.connectionId ,
sourceKey : job.sourceKey ,
syncId ,
} ) ,
) ;
2026-05-10 23:12:26 +02:00
memoryFlow ? . update ( {
connectionId : job.connectionId ,
adapter : job.sourceKey ,
sourceDir : stagedDir ,
syncId ,
} ) ;
memoryFlow ? . emit ( {
type : 'source_acquired' ,
adapter : job.sourceKey ,
trigger : job.trigger ,
fileCount : currentHashes.size ,
} ) ;
memoryFlow ? . emit ( { type : 'scope_detected' , fingerprint : scopeDescriptor?.fingerprint ? ? null } ) ;
memoryFlow ? . emit ( { type : 'raw_snapshot_written' , syncId , rawFileCount : currentHashes.size } ) ;
await sessionWorktree . git . commitFiles (
[ rawDirInWorktree ] ,
` ingest( ${ job . sourceKey } ): stage raw files syncId= ${ syncId } ` ,
this . deps . storage . systemGitAuthor . name ,
this . deps . storage . systemGitAuthor . email ,
) ;
await stage1 ? . updateProgress ( 0.5 , 'Checking what changed' ) ;
2026-05-18 13:38:06 +02:00
activePhase = 'diff' ;
const diffSet = await traceTimed (
trace ,
'diff' ,
'compute_diff_set' ,
{
connectionId : job.connectionId ,
sourceKey : job.sourceKey ,
currentHashCount : currentHashes.size ,
scopeFingerprint : scopeDescriptor?.fingerprint ? ? null ,
} ,
( ) = >
this . deps . diffSetService . compute (
job . connectionId ,
job . sourceKey ,
currentHashes ,
scopeDescriptor ? scopeDescriptor . isPathInScope . bind ( scopeDescriptor ) : undefined ,
) ,
2026-05-10 23:12:26 +02:00
) ;
const diffSummary = {
added : diffSet.added.length ,
modified : diffSet.modified.length ,
deleted : diffSet.deleted.length ,
unchanged : diffSet.unchanged.length ,
} ;
2026-05-18 13:38:06 +02:00
latestDiffSummary = diffSummary ;
2026-05-10 23:12:26 +02:00
memoryFlow ? . emit ( { type : 'diff_computed' , . . . diffSummary } ) ;
2026-05-18 13:38:06 +02:00
runRow = await this . deps . runs . create ( {
2026-05-10 23:12:26 +02:00
jobId : job.jobId ,
connectionId : job.connectionId ,
sourceKey : job.sourceKey ,
syncId ,
trigger : job.trigger ,
scopeFingerprint : scopeDescriptor?.fingerprint ? ? null ,
} ) ;
memoryFlow ? . update ( { runId : runRow.id } ) ;
const ingestToolMetadata = {
runId : runRow.id ,
jobId : job.jobId ,
syncId ,
sourceKey : job.sourceKey ,
} ;
2026-05-18 13:38:06 +02:00
const runTrace = trace . withContext ( { runId : runRow.id , syncId } ) ;
activeTrace = runTrace ;
const createdRunRow = runRow ;
await runTrace . event ( 'debug' , 'snapshot' , 'input_snapshot' , {
baseSha ,
stagedDir ,
rawFileCount : currentHashes.size ,
rawDirInWorktree ,
diffSummary ,
scopeFingerprint : scopeDescriptor?.fingerprint ? ? null ,
} ) ;
2026-05-10 23:12:26 +02:00
await stage1 ? . updateProgress (
1.0 ,
` ${ diffSet . added . length } new, ${ diffSet . modified . length } changed, ${ diffSet . deleted . length } removed ` ,
) ;
2026-05-18 13:38:06 +02:00
activePhase = 'detect' ;
const detected = await traceTimed ( runTrace , 'detect' , 'adapter_detect' , { stagedDir , sourceKey : job.sourceKey } , ( ) = >
adapter . detect ( stagedDir ) ,
) ;
await runTrace . event ( 'debug' , 'detect' , 'adapter_detected' , { detected } ) ;
2026-05-10 23:12:26 +02:00
if ( ! detected ) {
await this . deps . runs . markFailed ( runRow . id ) ;
throw new Error ( ` source adapter ' ${ job . sourceKey } ' did not recognize staged dir ` ) ;
}
let contextReport : ContextEvidenceIndexSummary | null = null ;
if ( adapter . evidenceIndexing === 'documents' && this . deps . contextEvidenceIndex ) {
contextReport = await this . deps . contextEvidenceIndex . indexStagedDir ( {
stagedDir ,
runId : runRow.id ,
connectionId : job.connectionId ,
sourceKey : job.sourceKey ,
syncId ,
diffSet ,
currentHashes ,
} ) ;
}
const stage2 = ctx ? . startPhase ( 0.04 ) ;
await stage2 ? . updateProgress ( 0.0 , 'Planning updates' ) ;
2026-05-18 13:38:06 +02:00
activePhase = 'planning' ;
2026-05-10 23:12:26 +02:00
let workUnits : WorkUnit [ ] = [ ] ;
let eviction : EvictionUnit | undefined ;
let unresolvedCards : UnresolvedCardInfo [ ] | undefined ;
let sourceContextReport : { capped? : boolean ; warnings? : string [ ] } | undefined ;
let parseArtifacts : unknown ;
2026-05-20 14:17:10 +02:00
let finalizationOutcome : IngestReportFinalizationOutcome | undefined ;
2026-05-13 13:43:23 +02:00
let wikiSlRefRepairResult : WikiSlRefRepairResult | null = null ;
2026-05-10 23:12:26 +02:00
let reconcileNotes : string [ ] = [ ] ;
let triageResult : PageTriageRunResult | null = null ;
if ( overrideReport ) {
eviction =
overrideReport . body . evictionInputs . length > 0
? { deletedRawPaths : overrideReport.body.evictionInputs }
: undefined ;
unresolvedCards = overrideReport . body . unresolvedCards ;
await stage2 ? . updateProgress ( 1.0 , ` Loaded prior report ${ overrideReport . jobId } for override reconciliation ` ) ;
} else {
2026-05-18 13:38:06 +02:00
const chunk = await traceTimed (
runTrace ,
'planning' ,
'chunk_work_units' ,
{
stagedDir ,
added : diffSet.added.length ,
modified : diffSet.modified.length ,
deleted : diffSet.deleted.length ,
} ,
( ) = > adapter . chunk ( stagedDir , diffSet ) ,
) ;
2026-05-10 23:12:26 +02:00
workUnits = chunk . workUnits ;
eviction = chunk . eviction ;
unresolvedCards = chunk . unresolvedCards ;
sourceContextReport = chunk . contextReport ;
parseArtifacts = chunk . parseArtifacts ;
reconcileNotes = chunk . reconcileNotes ? ? [ ] ;
triageResult =
contextReport && adapter . triageSupported && this . deps . pageTriage
? await this . deps . pageTriage . triageRun ( {
stagedDir ,
runId : runRow.id ,
connectionId : job.connectionId ,
sourceKey : job.sourceKey ,
syncId ,
jobId : job.jobId ,
diffSet ,
adapter ,
} )
: null ;
workUnits = this . filterWorkUnitsForTriage ( workUnits , triageResult ) ;
if ( adapter . clusterWorkUnits && workUnits . length > 0 ) {
workUnits = await adapter . clusterWorkUnits ( {
workUnits ,
stagedDir ,
embedding : this.deps.embedding ,
} ) ;
}
await stage2 ? . updateProgress ( 1.0 , ` Planned ${ workUnits . length } update ${ workUnits . length === 1 ? '' : 's' } ` ) ;
}
2026-05-18 13:38:06 +02:00
await runTrace . event ( 'debug' , 'planning' , 'work_units_planned' , {
workUnitCount : workUnits.length ,
evictionCount : eviction?.deletedRawPaths.length ? ? 0 ,
unresolvedCardCount : unresolvedCards?.length ? ? 0 ,
triageEnabled : triageResult?.enabled ? ? false ,
} ) ;
2026-05-10 23:12:26 +02:00
const targetConnectionIds = new Set < string > ( [ job . connectionId ] ) ;
if ( ! overrideReport && adapter . listTargetConnectionIds ) {
for ( const connectionId of await adapter . listTargetConnectionIds ( stagedDir ) ) {
targetConnectionIds . add ( connectionId ) ;
}
}
if ( overrideReport ) {
for ( const wu of overrideReport . body . workUnits ) {
for ( const action of wu . actions ) {
if ( action . target === 'sl' && action . targetConnectionId ) {
targetConnectionIds . add ( action . targetConnectionId ) ;
}
}
for ( const touched of wu . touchedSlSources ) {
targetConnectionIds . add ( touched . connectionId ) ;
}
}
}
const slConnectionIds = [ . . . targetConnectionIds ] . sort ( ) ;
2026-05-18 13:38:06 +02:00
await runTrace . event ( 'debug' , 'planning' , 'target_connections_resolved' , {
connectionIds : slConnectionIds ,
} ) ;
2026-05-10 23:12:26 +02:00
// Build shared per-job context.
const [ wikiIndex , slIndex ] = await Promise . all ( [ this . buildWikiIndex ( ) , this . buildSlIndex ( slConnectionIds ) ] ) ;
const baseFraming = await this . deps . promptService . loadPrompt ( 'memory_agent_bundle_ingest_work_unit' ) ;
const wuSkillNames = Array . from (
2026-05-13 16:05:58 +02:00
new Set < string > ( [ . . . adapter . skillNames , 'ingest_triage' , 'sl_capture' , 'wiki_capture' ] ) ,
2026-05-10 23:12:26 +02:00
) ;
const wuSkills = await this . deps . skillsRegistry . listSkills ( wuSkillNames , 'memory_agent' ) ;
const skillsPrompt = this . deps . skillsRegistry . buildSkillsPrompt ( wuSkills , 'memory_agent' ) ;
const canonicalPins = await this . deps . canonicalPins . listPins ( slConnectionIds ) ;
const workUnitOutcomes : WorkUnitOutcome [ ] = [ ] ;
const failedWorkUnits : string [ ] = [ ] ;
const stageIndex : StageIndex = overrideReport
? buildStageIndexFromReportBody ( job . jobId , overrideReport . connectionId , overrideReport . body )
: {
jobId : job.jobId ,
connectionId : job.connectionId ,
workUnits : [ ] ,
conflictsResolved : [ ] ,
evictionsApplied : [ ] ,
unmappedFallbacks : [ ] ,
artifactResolutions : [ ] ,
} ;
const memoryFlowPlannedWorkUnits = overrideReport
? stageIndex . workUnits . map ( stageIndexWorkUnitToMemoryFlowPlannedWorkUnit )
: workUnits . map ( workUnitToMemoryFlowPlannedWorkUnit ) ;
memoryFlow ? . update ( { plannedWorkUnits : memoryFlowPlannedWorkUnits } ) ;
memoryFlow ? . emit ( {
type : 'chunks_planned' ,
chunkCount : memoryFlowPlannedWorkUnits.length ,
workUnitCount : memoryFlowPlannedWorkUnits.length ,
evictionCount : eviction?.deletedRawPaths.length ? ? 0 ,
} ) ;
2026-05-18 13:38:06 +02:00
const isolatedDiffEnabled = ! overrideReport ;
const isolatedDiffSummary = {
enabled : isolatedDiffEnabled ,
integrationWorktreePath : isolatedDiffEnabled ? sessionWorktree.workdir : undefined ,
ingestionBaseSha : undefined as string | undefined ,
projectionSha : null as string | null ,
acceptedPatches : 0 ,
textualConflicts : 0 ,
semanticConflicts : 0 ,
resolverAttempts : 0 ,
resolverRepairs : 0 ,
resolverFailures : 0 ,
gateRepairAttempts : 0 ,
gateRepairs : 0 ,
gateRepairFailures : 0 ,
} ;
latestIsolatedDiffSummary = isolatedDiffSummary ;
2026-05-10 23:12:26 +02:00
const stage3 = ctx ? . startPhase ( 0.6 ) ;
await stage3 ? . updateProgress ( 0.0 , ` Processing ${ workUnits . length } update ${ workUnits . length === 1 ? '' : 's' } ` ) ;
2026-05-18 13:38:06 +02:00
activePhase = 'work_units' ;
2026-05-10 23:12:26 +02:00
this . logger . log ( ` [ingest-bundle] job= ${ job . jobId } tool-call transcripts: ${ transcriptDir } / ` ) ;
2026-05-18 13:38:06 +02:00
let projectionTouchedSources : TouchedSlSource [ ] = [ ] ;
let projectionChangedWikiPageKeys : string [ ] = [ ] ;
let projectionTouchedPaths : string [ ] = [ ] ;
2026-05-10 23:12:26 +02:00
if ( ! overrideReport ) {
2026-05-18 13:38:06 +02:00
await runTrace . event ( 'info' , 'routing' , 'isolated_diff_enabled' , {
sourceKey : job.sourceKey ,
workUnitCount : workUnits.length ,
integrationWorktreePath : sessionWorktree.workdir ,
} ) ;
if ( adapter . project ) {
const preProjectionSha = await sessionWorktree . git . revParseHead ( ) ;
const projection = await traceTimed (
runTrace ,
'projection' ,
'deterministic_projection' ,
{ sourceKey : job.sourceKey } ,
( ) = >
adapter . project ! ( {
connectionId : job.connectionId ,
sourceKey : job.sourceKey ,
syncId ,
jobId : job.jobId ,
runId : createdRunRow.id ,
stagedDir ,
workdir : sessionWorktree.workdir ,
parseArtifacts ,
semanticLayerService : this.deps.semanticLayerService ,
} ) ,
) ;
if ( projection . errors . length > 0 ) {
await this . deps . runs . markFailed ( runRow . id ) ;
throw new Error ( ` deterministic projection failed: ${ projection . errors . join ( '; ' ) } ` ) ;
}
projectionTouchedSources = projection . touchedSources ;
projectionChangedWikiPageKeys = projection . changedWikiPageKeys ;
const projectionPaths = [
. . . projection . touchedSources . map ( ( source ) = > ` semantic-layer/ ${ source . connectionId } / ${ source . sourceName } .yaml ` ) ,
. . . projection . changedWikiPageKeys . map ( ( pageKey ) = > ` wiki/global/ ${ pageKey } .md ` ) ,
] ;
projectionTouchedPaths = projectionPaths ;
const projectionCommit =
projectionPaths . length > 0
? await sessionWorktree . git . commitFiles (
projectionPaths ,
` ingest( ${ job . sourceKey } ): deterministic projection syncId= ${ syncId } ` ,
this . deps . storage . systemGitAuthor . name ,
this . deps . storage . systemGitAuthor . email ,
)
: await sessionWorktree . git . commitStaged (
` ingest( ${ job . sourceKey } ): deterministic projection syncId= ${ syncId } ` ,
this . deps . storage . systemGitAuthor . name ,
this . deps . storage . systemGitAuthor . email ,
) ;
isolatedDiffSummary . projectionSha =
projectionCommit . created || projectionCommit . commitHash !== preProjectionSha
? projectionCommit . commitHash
: null ;
await runTrace . event ( 'debug' , 'projection' , 'deterministic_projection_committed' , {
projectionSha : isolatedDiffSummary.projectionSha ,
touchedSources : projectionTouchedSources ,
changedWikiPageKeys : projectionChangedWikiPageKeys ,
warnings : projection.warnings ,
} ) ;
}
const ingestionBaseSha = await sessionWorktree . git . revParseHead ( ) ;
isolatedDiffSummary . ingestionBaseSha = ingestionBaseSha ;
const patchDir = join ( this . deps . storage . homeDir , 'ingest-patches' , job . jobId ) ;
2026-05-10 23:12:26 +02:00
const workUnitSettings = {
maxConcurrency : this.deps.settings.workUnitMaxConcurrency ? ? 1 ,
stepBudget : this.deps.settings.workUnitStepBudget ? ? 40 ,
failureMode : this.deps.settings.workUnitFailureMode ? ? 'continue' ,
} ;
const limitWorkUnit = pLimit ( workUnitSettings . maxConcurrency ) ;
const workUnitOutcomesByIndex : WorkUnitOutcome [ ] = [ ] ;
let completedWorkUnits = 0 ;
if ( workUnits . length === 0 ) {
await stage3 ? . updateProgress ( 1.0 , '0 of 0 work units complete' ) ;
}
try {
await Promise . all (
workUnits . map ( ( wu , index ) = >
limitWorkUnit ( async ( ) = > {
2026-05-18 13:38:06 +02:00
const outcome = await runIsolatedWorkUnit ( {
unitIndex : index ,
ingestionBaseSha ,
sessionWorktreeService : this.deps.sessionWorktreeService ,
patchDir ,
trace : runTrace ,
workUnit : wu ,
afterSuccess : ( child ) = > copyTransientIngestEvidence ( child . workdir , sessionWorktree . workdir ) ,
run : async ( child ) = > {
const scopedWikiService = this . deps . wikiService . forWorktree ( child . workdir ) ;
const scopedSemanticLayerService = this . deps . semanticLayerService . forWorktree ( child . workdir ) ;
return this . runWorkUnitInWorktree ( {
job ,
syncId ,
wu ,
worktree : child ,
stagedDir ,
contextReport ,
ingestToolMetadata ,
slConnectionIds ,
wikiIndex ,
slIndex ,
priorProvenance : await this . deps . provenance . findLatestArtifactsForRawPaths (
job . connectionId ,
job . sourceKey ,
wu . rawFiles ,
) ,
scopedWikiService ,
scopedSemanticLayerService ,
baseFraming ,
skillsPrompt ,
canonicalPins ,
workUnitSettings ,
transcriptDir ,
transcriptSummaries ,
recordTranscriptEntry ,
stageIndex ,
includeContextEvidenceTools : adapter.evidenceIndexing === 'documents' && ! ! contextReport ,
currentTableExists : ( tableRef ) = >
this . tableRefExistsInSemanticLayer ( scopedSemanticLayerService , slConnectionIds , tableRef ) ,
memoryFlow ,
wuSkillNames ,
onStepFinish : ( { stepIndex , stepBudget } ) = > {
memoryFlow ? . emit ( { type : 'work_unit_step' , unitKey : wu.unitKey , stepIndex , stepBudget } ) ;
} ,
} ) ;
} ,
} ) ;
2026-05-10 23:12:26 +02:00
workUnitOutcomesByIndex [ index ] = outcome ;
for ( const action of outcome . actions ) {
memoryFlow ? . emit ( {
type : 'candidate_action' ,
unitKey : outcome.unitKey ,
target : action.target ,
action : action.type ,
key : action.key ,
} ) ;
}
memoryFlow ? . emit ( {
type : 'work_unit_finished' ,
unitKey : outcome.unitKey ,
status : outcome.status ,
. . . ( outcome . reason ? { reason : outcome.reason } : { } ) ,
} ) ;
completedWorkUnits += 1 ;
await stage3 ? . updateProgress (
completedWorkUnits / workUnits . length ,
` ${ completedWorkUnits } of ${ workUnits . length } work units complete ` ,
) ;
} ) ,
) ,
) ;
} catch ( error ) {
await this . deps . runs . markFailed ( runRow . id ) ;
throw error ;
}
workUnitOutcomes . push (
. . . workUnitOutcomesByIndex . filter ( ( outcome ) : outcome is WorkUnitOutcome = > Boolean ( outcome ) ) ,
) ;
failedWorkUnits . push (
. . . workUnitOutcomes . filter ( ( outcome ) = > outcome . status === 'failed' ) . map ( ( outcome ) = > outcome . unitKey ) ,
) ;
2026-05-18 13:38:06 +02:00
latestWorkUnits = workUnitOutcomes ;
latestFailedWorkUnits = failedWorkUnits ;
2026-05-10 23:12:26 +02:00
stageIndex . workUnits = workUnitOutcomes . map ( ( o ) = > ( {
unitKey : o.unitKey ,
rawFiles : workUnits.find ( ( w ) = > w . unitKey === o . unitKey ) ? . rawFiles ? ? [ ] ,
status : o.status ,
reason : o.reason ,
actions : o.actions ,
touchedSlSources : o.touchedSlSources ,
slDisallowed : o.slDisallowed ,
slDisallowedReason : o.slDisallowedReason ,
} ) ) ;
2026-05-18 13:38:06 +02:00
activePhase = 'integration' ;
const integrablePatchCount = workUnitOutcomesByIndex . filter (
( outcome ) = > outcome ? . status === 'success' && ! ! outcome . patchPath ,
) . length ;
let integratedPatchCount = 0 ;
for ( const [ index , outcome ] of workUnitOutcomesByIndex . entries ( ) ) {
if ( ! outcome || outcome . status !== 'success' || ! outcome . patchPath ) {
continue ;
}
const wu = workUnits [ index ] ;
if ( ! wu ) {
continue ;
}
const integrationFailureDetails = {
unitKey : outcome.unitKey ,
patchPath : outcome.patchPath ,
allowedTargetConnectionIds : slConnectionIds ,
} ;
activeFailureDetails = integrationFailureDetails ;
emitStageProgress (
'integration' ,
80 ,
` Integrating ${ integratedPatchCount + 1 } / ${ integrablePatchCount } patches: ${ outcome . unitKey } ` ,
) ;
const integration = await integrateWorkUnitPatch ( {
unitKey : outcome.unitKey ,
patchPath : outcome.patchPath ,
integrationGit : sessionWorktree.git ,
trace : runTrace ,
author : this.deps.storage.systemGitAuthor ,
slDisallowed : wu.slDisallowed === true ,
allowedTargetConnectionIds : new Set ( slConnectionIds ) ,
validateAppliedTree : async ( touchedPaths ) = > {
await validateFinalIngestArtifacts ( {
connectionIds : slConnectionIds ,
changedWikiPageKeys : this.wikiPageKeysFromPaths ( touchedPaths ) ,
touchedSlSources : this.touchedSlSourcesFromPaths ( touchedPaths ) ,
wikiService : this.deps.wikiService.forWorktree ( sessionWorktree . workdir ) ,
semanticLayerService : this.deps.semanticLayerService.forWorktree ( sessionWorktree . workdir ) ,
validateTouchedSources : ( touched ) = >
validateWuTouchedSources (
{
semanticLayerService : this.deps.semanticLayerService.forWorktree ( sessionWorktree . workdir ) ,
connections : this.deps.connections ,
configService : sessionWorktree.config ,
gitService : sessionWorktree.git ,
slSourcesRepository : this.deps.slSourcesRepository ,
probeRowCount : this.deps.settings.probeRowCount ,
slValidator : this.deps.slValidator ,
} ,
touched ,
) ,
tableExists : ( connectionId , tableRef ) = >
this . tableRefExistsInSemanticLayer (
this . deps . semanticLayerService . forWorktree ( sessionWorktree . workdir ) ,
[ connectionId ] ,
tableRef ,
) ,
} ) ;
} ,
resolveTextualConflict : async ( context ) = > {
emitStageProgress ( 'integration' , 81 , ` Resolving text conflict for ${ context . unitKey } ` ) ;
const result = await resolveTextualConflict ( {
agentRunner : this.deps.agentRunner ,
workdir : sessionWorktree.workdir ,
unitKey : context.unitKey ,
patchPath : context.patchPath ,
touchedPaths : context.touchedPaths ,
trace : runTrace ,
reason : context.reason ,
maxAttempts : 1 ,
stepBudget : 12 ,
} ) ;
emitStageProgress (
'integration' ,
82 ,
result . status === 'repaired'
? ` Resolved text conflict for ${ context . unitKey } `
: ` Text conflict resolver failed for ${ context . unitKey } ` ,
) ;
return result ;
} ,
repairGateFailure : async ( context ) = > {
emitStageProgress ( 'integration' , 82 , ` Repairing semantic gate for ${ context . unitKey } ` ) ;
const result = await repairFinalGateFailure ( {
agentRunner : this.deps.agentRunner ,
workdir : sessionWorktree.workdir ,
gateError : context.reason ,
allowedPaths : context.touchedPaths ,
trace : runTrace ,
repairKind : 'patch_semantic_gate' ,
maxAttempts : 1 ,
stepBudget : 16 ,
} ) ;
emitStageProgress (
'integration' ,
83 ,
result . status === 'repaired'
? ` Repaired semantic gate for ${ context . unitKey } `
: ` Semantic gate repair failed for ${ context . unitKey } ` ,
) ;
return result ;
} ,
} ) ;
if ( integration . textualResolution ) {
isolatedDiffSummary . resolverAttempts += integration . textualResolution . attempts ;
if ( integration . textualResolution . status === 'repaired' ) {
isolatedDiffSummary . textualConflicts += 1 ;
isolatedDiffSummary . resolverRepairs += 1 ;
} else {
isolatedDiffSummary . resolverFailures += 1 ;
}
}
if ( integration . gateRepair ) {
isolatedDiffSummary . gateRepairAttempts += integration . gateRepair . attempts ;
if ( integration . gateRepair . status === 'repaired' ) {
isolatedDiffSummary . semanticConflicts += 1 ;
isolatedDiffSummary . gateRepairs += 1 ;
} else {
isolatedDiffSummary . gateRepairFailures += 1 ;
}
}
if ( integration . status === 'textual_conflict' ) {
isolatedDiffSummary . textualConflicts += 1 ;
await this . deps . runs . markFailed ( runRow . id ) ;
cleanupOutcome = 'conflict' ;
activeFailureDetails = {
. . . integrationFailureDetails ,
touchedPaths : integration.touchedPaths ,
reason : integration.reason ,
} ;
throw new Error ( ` isolated diff textual conflict in ${ outcome . unitKey } : ${ integration . reason } ` ) ;
}
if ( integration . status === 'semantic_conflict' ) {
isolatedDiffSummary . semanticConflicts += 1 ;
await this . deps . runs . markFailed ( runRow . id ) ;
cleanupOutcome = 'conflict' ;
activeFailureDetails = {
. . . integrationFailureDetails ,
touchedPaths : integration.touchedPaths ,
reason : integration.reason ,
} ;
throw new Error ( ` isolated diff semantic conflict in ${ outcome . unitKey } : ${ integration . reason } ` ) ;
}
activeFailureDetails = undefined ;
if ( integration . touchedPaths . length > 0 ) {
isolatedDiffSummary . acceptedPatches += 1 ;
integratedPatchCount += 1 ;
}
emitStageProgress (
'integration' ,
83 ,
` Integrated ${ integratedPatchCount } / ${ integrablePatchCount } patches ` ,
) ;
}
2026-05-10 23:12:26 +02:00
}
const carryForwardResult =
contextReport && this . deps . contextCandidateCarryforward
? await this . deps . contextCandidateCarryforward . carryForward ( {
runId : runRow.id ,
connectionId : job.connectionId ,
sourceKey : job.sourceKey ,
} )
: null ;
const dedupResult =
contextReport && this . deps . candidateDedup ? await this . deps . candidateDedup . deduplicateRun ( runRow . id ) : null ;
2026-05-18 13:38:06 +02:00
const preReconciliationSha = await sessionWorktree . git . revParseHead ( ) ;
activePhase = 'reconciliation' ;
2026-05-10 23:12:26 +02:00
// Stage 4 — reconciliation. Shares scoped wiki/SL with a fresh CaptureSession
// so reconciliation writes land in the same worktree Stage 3 used.
const reconcileSession : CaptureSession = {
userId : 'system' ,
chatId : ` ${ job . jobId } -reconcile ` ,
userMessage : ` reconcile( ${ job . sourceKey } ) ` ,
connectionId : job.connectionId ,
userScopedEnabled : false ,
forceGlobalScope : true ,
touchedSlSources : createTouchedSlSources ( ) ,
preHead : await sessionWorktree . git . revParseHead ( ) ,
} ;
const reconcileActions : MemoryAction [ ] = [ ] ;
const rcScopedWiki = this . deps . wikiService . forWorktree ( sessionWorktree . workdir ) ;
const rcScopedSl = this . deps . semanticLayerService . forWorktree ( sessionWorktree . workdir ) ;
2026-05-12 16:56:58 -04:00
const reconciliationAllowedRawPaths = new Set < string > ( [
. . . currentHashes . keys ( ) ,
. . . ( eviction ? . deletedRawPaths ? ? [ ] ) ,
] ) ;
2026-05-10 23:12:26 +02:00
const rcToolSession : ToolSession = {
connectionId : job.connectionId ,
isWorktreeScoped : true ,
preHead : reconcileSession.preHead ,
touchedSlSources : reconcileSession.touchedSlSources ,
actions : reconcileActions ,
2026-05-12 16:56:58 -04:00
allowedRawPaths : reconciliationAllowedRawPaths ,
2026-05-13 13:43:23 +02:00
allowedConnectionNames : new Set ( slConnectionIds ) ,
2026-05-10 23:12:26 +02:00
semanticLayerService : rcScopedSl ,
wikiService : rcScopedWiki ,
configService : sessionWorktree.config ,
gitService : sessionWorktree.git ,
ingest : ingestToolMetadata ,
evictionDecisions : [ ] ,
} ;
const rcToolset = this . deps . toolsetFactory . createIngestWuToolset ( rcToolSession , {
includeContextEvidenceTools : adapter.evidenceIndexing === 'documents' && ! ! contextReport ,
} ) ;
const rcToolContext : ToolContext = {
sourceId : 'ingest' ,
messageId : ` ${ job . jobId } -reconcile ` ,
userId : 'system' ,
connectionId : job.connectionId ,
ingest : ingestToolMetadata ,
session : rcToolSession ,
} ;
2026-05-16 12:06:34 +02:00
const rcLoadSkill : KtxRuntimeToolSet = {
load_skill : {
name : 'load_skill' ,
2026-05-10 23:12:26 +02:00
description : 'Load a skill.' ,
inputSchema : z.object ( { name : z.string ( ) } ) ,
execute : async ( { name } ) = > {
const skill = await this . deps . skillsRegistry . getSkill ( name , 'memory_agent' ) ;
if ( ! skill ) {
2026-05-16 12:06:34 +02:00
return { markdown : ` Skill " ${ name } " not found ` } ;
2026-05-10 23:12:26 +02:00
}
const body = await readFile ( join ( skill . path , 'SKILL.md' ) , 'utf-8' ) ;
2026-05-16 12:06:34 +02:00
const structured = { name : skill.name , content : this.deps.skillsRegistry.stripFrontmatter ( body ) } ;
return { markdown : ` # ${ structured . name } \ n \ n ${ structured . content } ` , structured } ;
2026-05-10 23:12:26 +02:00
} ,
2026-05-16 12:06:34 +02:00
} ,
2026-05-10 23:12:26 +02:00
} ;
const allStagedPaths = new Set < string > ( [ . . . currentHashes . keys ( ) ] ) ;
2026-05-16 12:06:34 +02:00
const rcRawSpanTool = {
read_raw_span : createRuntimeToolDescriptorFromAiTool (
'read_raw_span' ,
createReadRawSpanTool ( { stagedDir , allowedPaths : allStagedPaths } ) ,
) ,
} ;
const rcStageListTool = {
stage_list : createRuntimeToolDescriptorFromAiTool ( 'stage_list' , createStageListTool ( { stageIndex } ) ) ,
} ;
const rcStageDiffTool = {
stage_diff : createRuntimeToolDescriptorFromAiTool ( 'stage_diff' , createStageDiffTool ( { stageIndex } ) ) ,
} ;
2026-05-10 23:12:26 +02:00
const rcEvictionListTool = {
2026-05-16 12:06:34 +02:00
eviction_list : createRuntimeToolDescriptorFromAiTool (
'eviction_list' ,
createEvictionListTool ( {
provenance : this.deps.provenance ,
connectionId : job.connectionId ,
sourceKey : job.sourceKey ,
deletedRawPaths : eviction?.deletedRawPaths ? ? [ ] ,
} ) ,
) ,
2026-05-10 23:12:26 +02:00
} ;
const rcEmitConflictResolutionTool = {
2026-05-16 12:06:34 +02:00
emit_conflict_resolution : createRuntimeToolDescriptorFromAiTool (
'emit_conflict_resolution' ,
createEmitConflictResolutionTool ( { stageIndex } ) ,
) ,
2026-05-10 23:12:26 +02:00
} ;
const rcEmitEvictionDecisionTool = {
2026-05-16 12:06:34 +02:00
emit_eviction_decision : createRuntimeToolDescriptorFromAiTool (
'emit_eviction_decision' ,
createEmitEvictionDecisionTool ( {
stageIndex ,
deletedRawPaths : eviction?.deletedRawPaths ? ? [ ] ,
} ) ,
) ,
2026-05-10 23:12:26 +02:00
} ;
const rcEmitArtifactResolutionTool = {
2026-05-16 12:06:34 +02:00
emit_artifact_resolution : createRuntimeToolDescriptorFromAiTool (
'emit_artifact_resolution' ,
createEmitArtifactResolutionTool ( {
stageIndex ,
allowedPaths : allStagedPaths ,
} ) ,
) ,
2026-05-10 23:12:26 +02:00
} ;
const rcEmitUnmappedFallbackTool = {
2026-05-16 12:06:34 +02:00
emit_unmapped_fallback : createRuntimeToolDescriptorFromAiTool (
'emit_unmapped_fallback' ,
createEmitUnmappedFallbackTool ( {
stageIndex ,
allowedPaths : allStagedPaths ,
tableRefExists : ( tableRef ) = > this . tableRefExistsInSemanticLayer ( rcScopedSl , slConnectionIds , tableRef ) ,
} ) ,
) ,
2026-05-10 23:12:26 +02:00
} ;
const reconcileBaseFraming = await this . deps . promptService . loadPrompt ( 'memory_agent_bundle_ingest_reconcile' ) ;
const reconcileSkills = await this . deps . skillsRegistry . listSkills (
Array . from (
2026-05-13 16:05:58 +02:00
new Set ( [ 'ingest_triage' , 'sl_capture' , 'wiki_capture' , . . . ( adapter . reconcileSkillNames ? ? [ ] ) ] ) ,
2026-05-10 23:12:26 +02:00
) ,
'memory_agent' ,
) ;
const reconcileSkillsPrompt = this . deps . skillsRegistry . buildSkillsPrompt ( reconcileSkills , 'memory_agent' ) ;
const relevantCanonicalPins = selectRelevantCanonicalPins ( stageIndex , canonicalPins ) ;
const stage4 = ctx ? . startPhase ( 0.16 ) ;
const hasCandidateReconcileWork = ( dedupResult ? . representatives . length ? ? 0 ) > 0 ;
const hasReconcileWork =
stageIndex . workUnits . some ( ( wu ) = > wu . actions . length > 0 ) ||
( eviction ? . deletedRawPaths . length ? ? 0 ) > 0 ||
hasCandidateReconcileWork ;
if ( hasReconcileWork || overrideReport ) {
2026-05-18 13:38:06 +02:00
emitStageProgress ( 'reconciliation' , 84 , 'Reconciling results' ) ;
2026-05-10 23:12:26 +02:00
await stage4 ? . updateProgress ( 0.0 , 'Reconciling results' ) ;
}
let curatorReport = null ;
let curatorWarnings : string [ ] = [ ] ;
let reconcileOutcome : Awaited < ReturnType < typeof runReconciliationStage4 > > ;
if ( contextReport && this . deps . curatorPagination ) {
const curatorOutcome = await this . deps . curatorPagination . reconcile ( {
runId : runRow.id ,
sourceKey : job.sourceKey ,
jobId : job.jobId ,
stageIndex ,
evictionUnit : eviction ,
representatives : dedupResult?.representatives ? ? [ ] ,
initialBudget : this.resolveContextCuratorBudget ( job . bundleRef , stageIndex ) ,
modelRole : 'curator' ,
buildSystemPrompt : ( ) = >
buildReconcileSystemPrompt ( {
baseFraming : reconcileBaseFraming ,
skillsPrompt : reconcileSkillsPrompt ,
syncId ,
sourceKey : job.sourceKey ,
canonicalPins : relevantCanonicalPins ,
} ) ,
buildUserPrompt : ( { summary , items , runState } ) = >
buildReconcileUserPrompt ( stageIndex , eviction , { summary , items } , reconcileNotes , runState ) ,
buildToolSet : ( _passNumber ) = >
wrapToolsWithLogger (
buildReconcileToolSet ( {
loadSkillTool : rcLoadSkill ,
stageListTool : rcStageListTool ,
stageDiffTool : rcStageDiffTool ,
evictionListTool : rcEvictionListTool ,
emitConflictResolutionTool : rcEmitConflictResolutionTool ,
emitEvictionDecisionTool : rcEmitEvictionDecisionTool ,
emitArtifactResolutionTool : rcEmitArtifactResolutionTool ,
emitUnmappedFallbackTool : rcEmitUnmappedFallbackTool ,
readRawSpanTool : rcRawSpanTool ,
2026-05-16 12:06:34 +02:00
toolsetTools : rcToolset.toRuntimeTools ( rcToolContext ) ,
2026-05-10 23:12:26 +02:00
} ) ,
join ( transcriptDir , 'reconcile.jsonl' ) ,
'reconcile' ,
{ onEntry : recordTranscriptEntry ( join ( transcriptDir , 'reconcile.jsonl' ) ) } ,
) ,
getReconciliationActions : ( ) = > reconcileActions ,
onStepFinish : stage4
? ( { passNumber , stepIndex , stepBudget } ) = > {
2026-05-18 13:38:06 +02:00
emitStageProgress (
'reconciliation' ,
85 ,
` Reconciling results: pass ${ passNumber } step ${ stepIndex } / ${ stepBudget } ` ,
{ transient : true } ,
) ;
2026-05-10 23:12:26 +02:00
void stage4 . updateProgress (
stepIndex / stepBudget ,
` Reconciling results · pass ${ passNumber } step ${ stepIndex } ` ,
) ;
}
: undefined ,
} ) ;
curatorReport = curatorOutcome . report ;
curatorWarnings = curatorOutcome . warnings ;
reconcileOutcome = {
skipped : curatorOutcome.skipped ,
stopReason : curatorOutcome.stopReason ,
error : curatorOutcome.error ,
} ;
} else {
reconcileOutcome = await runReconciliationStage4 ( {
stageIndex ,
evictionUnit : eviction ,
agentRunner : this.deps.agentRunner ,
buildSystemPrompt : ( ) = >
buildReconcileSystemPrompt ( {
baseFraming : reconcileBaseFraming ,
skillsPrompt : reconcileSkillsPrompt ,
syncId ,
sourceKey : job.sourceKey ,
canonicalPins : relevantCanonicalPins ,
} ) ,
buildUserPrompt : ( idx , ev ) = > buildReconcileUserPrompt ( idx , ev , undefined , reconcileNotes ) ,
buildToolSet : ( ) = >
wrapToolsWithLogger (
buildReconcileToolSet ( {
loadSkillTool : rcLoadSkill ,
stageListTool : rcStageListTool ,
stageDiffTool : rcStageDiffTool ,
evictionListTool : rcEvictionListTool ,
emitConflictResolutionTool : rcEmitConflictResolutionTool ,
emitEvictionDecisionTool : rcEmitEvictionDecisionTool ,
emitArtifactResolutionTool : rcEmitArtifactResolutionTool ,
emitUnmappedFallbackTool : rcEmitUnmappedFallbackTool ,
readRawSpanTool : rcRawSpanTool ,
2026-05-16 12:06:34 +02:00
toolsetTools : rcToolset.toRuntimeTools ( rcToolContext ) ,
2026-05-10 23:12:26 +02:00
} ) ,
join ( transcriptDir , 'reconcile.jsonl' ) ,
'reconcile' ,
{ onEntry : recordTranscriptEntry ( join ( transcriptDir , 'reconcile.jsonl' ) ) } ,
) ,
modelRole : 'reconcile' ,
stepBudget : 60 ,
sourceKey : job.sourceKey ,
jobId : job.jobId ,
force : ! ! overrideReport ,
onStepFinish : stage4
? ( { stepIndex , stepBudget } ) = > {
2026-05-18 13:38:06 +02:00
emitStageProgress ( 'reconciliation' , 85 , ` Reconciling results: step ${ stepIndex } / ${ stepBudget } ` , {
transient : true ,
} ) ;
2026-05-10 23:12:26 +02:00
void stage4 . updateProgress ( stepIndex / stepBudget , ` Reconciling results · step ${ stepIndex } ` ) ;
}
: undefined ,
} ) ;
}
2026-05-18 13:38:06 +02:00
latestReconciliationSkipped = reconcileOutcome . skipped ;
2026-05-10 23:12:26 +02:00
2026-05-17 10:10:14 +02:00
const danglingReconcileWikiRefs = await findDanglingWikiRefsForActions ( {
wikiService : rcScopedWiki ,
scope : 'GLOBAL' ,
scopeId : null ,
actions : reconcileActions ,
} ) ;
if ( danglingReconcileWikiRefs . length > 0 ) {
await this . deps . runs . markFailed ( runRow . id ) ;
throw new Error ( ` wiki references target missing page(s): ${ danglingReconcileWikiRefs . join ( ', ' ) } ` ) ;
}
2026-05-10 23:12:26 +02:00
const candidateSummaryAfterReconcile =
contextReport && this . deps . contextEvidenceCandidates
? await this . deps . contextEvidenceCandidates . getCandidateSummary ( runRow . id )
: null ;
memoryFlow ? . emit ( {
type : 'reconciliation_finished' ,
conflictCount : stageIndex.conflictsResolved.length ,
fallbackCount : stageIndex.unmappedFallbacks.length ,
} ) ;
2026-05-18 13:38:06 +02:00
await runTrace . event ( 'debug' , 'reconciliation' , 'reconciliation_finished' , {
skipped : reconcileOutcome.skipped ,
stopReason : reconcileOutcome.stopReason ? ? null ,
actionCount : reconcileActions.length ,
conflictCount : stageIndex.conflictsResolved.length ,
fallbackCount : stageIndex.unmappedFallbacks.length ,
artifactResolutionCount : stageIndex.artifactResolutions?.length ? ? 0 ,
} ) ;
2026-05-10 23:12:26 +02:00
await stage4 ? . updateProgress ( 1.0 , reconcileOutcome . skipped ? 'No reconciliation needed' : 'Reconciled' ) ;
2026-05-20 14:17:10 +02:00
const preFinalizationSha = await sessionWorktree . git . revParseHead ( ) ;
const preFinalizationSourcesByConnection = await this . loadSourcesByConnection (
sessionWorktree . workdir ,
slConnectionIds ,
) ;
let finalizationActions : MemoryAction [ ] = [ ] ;
let finalizationTouchedPaths : string [ ] = [ ] ;
let finalizationTouchedSources : TouchedSlSource [ ] = [ ] ;
let finalizationChangedWikiPageKeys : string [ ] = [ ] ;
let finalizationSha : string | null = null ;
activePhase = 'finalization' ;
if ( adapter . finalize ) {
const stageFinalization = ctx ? . startPhase ( 0.04 ) ;
emitStageProgress ( 'finalization' , 87 , 'Running deterministic finalization' ) ;
await stageFinalization ? . updateProgress ( 0.0 , 'Running deterministic finalization' ) ;
await runTrace . event ( 'debug' , 'finalization' , 'finalization_started' , { sourceKey : job.sourceKey } ) ;
const result = await adapter . finalize ( {
connectionId : job.connectionId ,
sourceKey : job.sourceKey ,
syncId ,
jobId : job.jobId ,
runId : createdRunRow.id ,
stagedDir ,
workdir : sessionWorktree.workdir ,
. . . ( overrideReport ? { } : { parseArtifacts } ) ,
stageIndex ,
workUnitOutcomes ,
reconciliationActions : reconcileActions ,
. . . ( overrideReport
? {
overrideReplay : {
priorJobId : overrideReport.jobId ,
priorRunId : overrideReport.runId ,
priorSyncId : overrideReport.body.syncId ,
evictionRawPaths : overrideReport.body.evictionInputs ,
} ,
}
: { } ) ,
} ) ;
if ( result . errors . length > 0 ) {
finalizationOutcome = {
2026-05-10 23:12:26 +02:00
sourceKey : job.sourceKey ,
2026-05-20 14:17:10 +02:00
status : 'failed' ,
commitSha : null ,
touchedPaths : [ ] ,
declaredTouchedSources : result.touchedSources ,
derivedTouchedSources : [ ] ,
declaredChangedWikiPageKeys : result.changedWikiPageKeys ,
derivedChangedWikiPageKeys : [ ] ,
mismatches : [ ] ,
2026-05-10 23:12:26 +02:00
result : result.result ,
errors : result.errors ,
warnings : result.warnings ,
2026-05-20 14:17:10 +02:00
actions : result.actions ? ? [ ] ,
provenanceExclusions : [ ] ,
2026-05-10 23:12:26 +02:00
} ;
2026-05-20 14:17:10 +02:00
latestFinalizationOutcome = finalizationOutcome ;
await runTrace . event ( 'error' , 'finalization' , 'finalization_failed' , {
sourceKey : job.sourceKey ,
errors : result.errors ,
warnings : result.warnings ,
} ) ;
throw new Error ( ` deterministic finalization failed: ${ result . errors . join ( '; ' ) } ` ) ;
}
const changedBeforeFinalization = new Set ( [
. . . projectionTouchedPaths ,
. . . workUnitOutcomes . flatMap ( ( outcome ) = > outcome . patchTouchedPaths ? ? [ ] ) ,
. . . ( preReconciliationSha && preFinalizationSha !== preReconciliationSha
? ( await sessionWorktree . git . diffNameStatus ( preReconciliationSha , preFinalizationSha ) ) . map (
( entry ) = > entry . path ,
)
: [ ] ) ,
] ) ;
finalizationTouchedPaths = await sessionWorktree . git . changedPaths ( ) ;
const overlapping = finalizationTouchedPaths . filter ( ( path ) = > changedBeforeFinalization . has ( path ) ) ;
if ( overlapping . length > 0 ) {
await runTrace . event ( 'error' , 'finalization' , 'finalization_failed' , {
sourceKey : job.sourceKey ,
reason : 'path_overlap' ,
overlappingPaths : overlapping.sort ( ) ,
} ) ;
throw new Error (
` finalization modified path(s) already changed earlier in this run: ${ overlapping . sort ( ) . join ( ', ' ) } ` ,
) ;
}
const finalizationCommit =
finalizationTouchedPaths . length > 0
? await sessionWorktree . git . commitFiles (
finalizationTouchedPaths ,
` ingest( ${ job . sourceKey } ): deterministic finalization syncId= ${ syncId } ` ,
this . deps . storage . systemGitAuthor . name ,
this . deps . storage . systemGitAuthor . email ,
)
: await sessionWorktree . git . commitStaged (
` ingest( ${ job . sourceKey } ): deterministic finalization syncId= ${ syncId } ` ,
this . deps . storage . systemGitAuthor . name ,
this . deps . storage . systemGitAuthor . email ,
) ;
finalizationSha = finalizationCommit . created ? finalizationCommit.commitHash : null ;
const postFinalizationSha = await sessionWorktree . git . revParseHead ( ) ;
finalizationTouchedPaths =
preFinalizationSha !== postFinalizationSha
? ( await sessionWorktree . git . diffNameStatus ( preFinalizationSha , postFinalizationSha ) ) . map (
( entry ) = > entry . path ,
)
: [ ] ;
const changedConnectionIds = [
. . . new Set ( [
. . . slConnectionIds ,
. . . finalizationTouchedPaths
. filter ( ( path ) = > path . startsWith ( 'semantic-layer/' ) )
. map ( ( path ) = > path . split ( '/' ) [ 1 ] )
. filter ( ( connectionId ) : connectionId is string = > Boolean ( connectionId ) ) ,
] ) ,
] . sort ( ) ;
const postFinalizationSourcesByConnection = await this . loadSourcesByConnection (
sessionWorktree . workdir ,
changedConnectionIds ,
) ;
const scope = await deriveFinalizationTouchedSources ( {
changedPaths : finalizationTouchedPaths ,
beforeSourcesByConnection : preFinalizationSourcesByConnection ,
afterSourcesByConnection : postFinalizationSourcesByConnection ,
} ) ;
if ( scope . unresolvedPaths . length > 0 ) {
await runTrace . event ( 'error' , 'finalization' , 'finalization_failed' , {
sourceKey : job.sourceKey ,
reason : 'unresolved_semantic_layer_paths' ,
unresolvedPaths : scope.unresolvedPaths ,
} ) ;
throw new Error ( ` could not resolve finalization semantic-layer path(s): ${ scope . unresolvedPaths . join ( ', ' ) } ` ) ;
}
finalizationTouchedSources = scope . touchedSources ;
finalizationChangedWikiPageKeys = deriveFinalizationWikiPageKeys ( finalizationTouchedPaths ) ;
const mismatches = compareFinalizationDeclarations ( {
declaredTouchedSources : result.touchedSources ,
derivedTouchedSources : finalizationTouchedSources ,
declaredChangedWikiPageKeys : result.changedWikiPageKeys ,
derivedChangedWikiPageKeys : finalizationChangedWikiPageKeys ,
} ) ;
if ( mismatches . length > 0 ) {
finalizationOutcome = {
2026-05-10 23:12:26 +02:00
sourceKey : job.sourceKey ,
status : 'failed' ,
2026-05-20 14:17:10 +02:00
commitSha : finalizationSha ,
touchedPaths : finalizationTouchedPaths ,
declaredTouchedSources : result.touchedSources ,
derivedTouchedSources : finalizationTouchedSources ,
declaredChangedWikiPageKeys : result.changedWikiPageKeys ,
derivedChangedWikiPageKeys : finalizationChangedWikiPageKeys ,
mismatches ,
result : result.result ,
errors : [ 'finalization touched artifact declaration mismatch' ] ,
warnings : result.warnings ,
actions : result.actions ? ? [ ] ,
provenanceExclusions : [ ] ,
2026-05-10 23:12:26 +02:00
} ;
2026-05-20 14:17:10 +02:00
latestFinalizationOutcome = finalizationOutcome ;
await runTrace . event ( 'error' , 'finalization' , 'finalization_failed' , {
sourceKey : job.sourceKey ,
reason : 'declaration_mismatch' ,
mismatches ,
} ) ;
throw new Error (
` finalization touched artifact declaration mismatch: ${ mismatches
. map ( ( mismatch ) = > ` ${ mismatch . direction } : ${ mismatch . artifactKind } : ${ mismatch . key } ` )
. join ( ', ' ) } ` ,
) ;
2026-05-10 23:12:26 +02:00
}
2026-05-20 14:17:10 +02:00
finalizationActions = result . actions ? ? [ ] ;
finalizationOutcome = {
sourceKey : job.sourceKey ,
status : 'success' ,
commitSha : finalizationSha ,
touchedPaths : finalizationTouchedPaths ,
declaredTouchedSources : result.touchedSources ,
derivedTouchedSources : finalizationTouchedSources ,
declaredChangedWikiPageKeys : result.changedWikiPageKeys ,
derivedChangedWikiPageKeys : finalizationChangedWikiPageKeys ,
mismatches ,
result : result.result ,
errors : [ ] ,
warnings : result.warnings ,
actions : finalizationActions ,
provenanceExclusions : [ ] ,
} ;
latestFinalizationOutcome = finalizationOutcome ;
emitStageProgress ( 'finalization' , 88 , 'Deterministic finalization complete' ) ;
await stageFinalization ? . updateProgress ( 1.0 , 'Deterministic finalization complete' ) ;
await runTrace . event ( 'debug' , 'finalization' , 'finalization_committed' , {
sourceKey : job.sourceKey ,
commitSha : finalizationSha ,
touchedPaths : finalizationTouchedPaths ,
touchedSources : finalizationTouchedSources ,
changedWikiPageKeys : finalizationChangedWikiPageKeys ,
warnings : result.warnings ,
} ) ;
} else {
await runTrace . event ( 'debug' , 'finalization' , 'finalization_skipped' , { sourceKey : job.sourceKey } ) ;
2026-05-10 23:12:26 +02:00
}
2026-05-13 13:43:23 +02:00
const repairConnectionIds = [
. . . new Set ( [
. . . slConnectionIds ,
2026-05-20 14:17:10 +02:00
. . . finalizationTouchedSources . map ( ( source ) = > source . connectionId ) ,
2026-05-13 13:43:23 +02:00
] ) ,
] . sort ( ) ;
2026-05-18 13:38:06 +02:00
activePhase = 'wiki_sl_ref_repair' ;
emitStageProgress ( 'wiki_sl_ref_repair' , 88 , 'Repairing wiki semantic-layer references' ) ;
wikiSlRefRepairResult = await traceTimed (
runTrace ,
'wiki_sl_ref_repair' ,
'wiki_sl_refs_repair' ,
{ connectionIds : repairConnectionIds } ,
( ) = >
repairWikiSlRefs ( {
wikiService : this.deps.wikiService.forWorktree ( sessionWorktree . workdir ) ,
semanticLayerService : this.deps.semanticLayerService.forWorktree ( sessionWorktree . workdir ) ,
configService : sessionWorktree.config ,
connectionIds : repairConnectionIds ,
} ) ,
) ;
await runTrace . event ( 'debug' , 'wiki_sl_ref_repair' , 'wiki_sl_refs_repaired' , {
repairCount : wikiSlRefRepairResult.repairs.length ,
repairs : wikiSlRefRepairResult.repairs ,
warnings : wikiSlRefRepairResult.warnings ,
} ) ;
emitStageProgress ( 'wiki_sl_ref_repair' , 88 , 'Checked wiki semantic-layer references' ) ;
const postReconciliationSha = await sessionWorktree . git . revParseHead ( ) ;
const postReconciliationPaths =
preReconciliationSha && postReconciliationSha && preReconciliationSha !== postReconciliationSha
? ( await sessionWorktree . git . diffNameStatus ( preReconciliationSha , postReconciliationSha ) ) . map ( ( entry ) = > entry . path )
: [ ] ;
const baseFinalChangedWikiPageKeys = this . uniqueWikiPageKeys ( [
. . . ( isolatedDiffEnabled ? projectionChangedWikiPageKeys : [ ] ) ,
. . . workUnitOutcomes
. flatMap ( ( outcome ) = > outcome . patchTouchedPaths ? ? [ ] )
. flatMap ( ( path ) = > this . wikiPageKeysFromPaths ( [ path ] ) ) ,
. . . this . wikiPageKeysFromActions ( reconcileActions ) ,
2026-05-20 14:17:10 +02:00
. . . finalizationChangedWikiPageKeys ,
2026-05-18 13:38:06 +02:00
. . . postReconciliationPaths . flatMap ( ( path ) = > this . wikiPageKeysFromPaths ( [ path ] ) ) ,
. . . wikiSlRefRepairResult . repairs . filter ( ( repair ) = > repair . scope === 'GLOBAL' ) . map ( ( repair ) = > repair . pageKey ) ,
] ) ;
const finalTouchedSlSources = this . uniqueTouchedSlSources ( [
. . . ( isolatedDiffEnabled ? projectionTouchedSources : [ ] ) ,
. . . workUnitOutcomes . flatMap ( ( outcome ) = > outcome . touchedSlSources ) ,
. . . this . touchedSlSourcesFromActions ( reconcileActions , job . connectionId ) ,
. . . this . touchedSlSourcesFromPaths ( postReconciliationPaths ) ,
2026-05-20 14:17:10 +02:00
. . . finalizationTouchedSources ,
2026-05-18 13:38:06 +02:00
] ) ;
const finalWikiGateScope = await this . wikiPageKeysForFinalGates ( {
2026-05-13 13:43:23 +02:00
wikiService : this.deps.wikiService.forWorktree ( sessionWorktree . workdir ) ,
2026-05-18 13:38:06 +02:00
changedWikiPageKeys : baseFinalChangedWikiPageKeys ,
touchedSlSources : finalTouchedSlSources ,
actions : [ . . . stageIndex . workUnits . flatMap ( ( wu ) = > wu . actions ) , . . . reconcileActions ] ,
2026-05-13 13:43:23 +02:00
} ) ;
2026-05-18 13:38:06 +02:00
const finalChangedWikiPageKeys = finalWikiGateScope . pageKeys ;
const finalTargetPolicyPaths = [
. . . projectionTouchedPaths ,
. . . workUnitOutcomes . flatMap ( ( outcome ) = > outcome . patchTouchedPaths ? ? [ ] ) ,
. . . postReconciliationPaths ,
2026-05-20 14:17:10 +02:00
. . . finalizationTouchedPaths ,
2026-05-18 13:38:06 +02:00
] ;
const targetPolicyTraceData = {
allowedTargetConnectionIds : slConnectionIds ,
touchedPaths : [ . . . new Set ( finalTargetPolicyPaths ) ] . sort ( ) ,
} ;
activePhase = 'target_policy' ;
activeFailureDetails = targetPolicyTraceData ;
emitStageProgress ( 'final_gates' , 88 , 'Checking semantic-layer target policy' ) ;
await traceTimed ( runTrace , 'target_policy' , 'semantic_layer_target_policy' , targetPolicyTraceData , async ( ) = > {
assertSemanticLayerTargetPathsAllowed ( {
paths : finalTargetPolicyPaths ,
allowedConnectionIds : new Set ( slConnectionIds ) ,
} ) ;
} ) ;
activeFailureDetails = undefined ;
const finalArtifactGateTraceData = {
changedWikiPageKeys : finalChangedWikiPageKeys ,
wikiReferenceGateScope : finalWikiGateScope.trace ,
touchedSlSources : finalTouchedSlSources ,
projectionTouchedPaths ,
workUnitPatchTouchedPaths : workUnitOutcomes.flatMap ( ( outcome ) = > outcome . patchTouchedPaths ? ? [ ] ) ,
actionOrigins : this.finalGateActionOrigins ( {
stageIndex ,
reconcileActions ,
fallbackConnectionId : job.connectionId ,
} ) ,
preReconciliationSha ,
postReconciliationSha ,
postReconciliationPaths ,
reconciliationActionCount : reconcileActions.length ,
wikiSlRefRepairCount : wikiSlRefRepairResult.repairs.length ,
} ;
activePhase = 'final_gates' ;
activeFailureDetails = finalArtifactGateTraceData ;
emitStageProgress ( 'final_gates' , 89 , 'Running final artifact gates' ) ;
try {
await traceTimed (
runTrace ,
'final_gates' ,
'final_artifact_gates' ,
finalArtifactGateTraceData ,
async ( ) = > {
await validateFinalIngestArtifacts ( {
connectionIds : repairConnectionIds ,
changedWikiPageKeys : finalChangedWikiPageKeys ,
touchedSlSources : finalTouchedSlSources ,
wikiService : this.deps.wikiService.forWorktree ( sessionWorktree . workdir ) ,
semanticLayerService : this.deps.semanticLayerService.forWorktree ( sessionWorktree . workdir ) ,
validateTouchedSources : ( touched ) = >
validateWuTouchedSources (
{
semanticLayerService : this.deps.semanticLayerService.forWorktree ( sessionWorktree . workdir ) ,
connections : this.deps.connections ,
configService : sessionWorktree.config ,
gitService : sessionWorktree.git ,
slSourcesRepository : this.deps.slSourcesRepository ,
probeRowCount : this.deps.settings.probeRowCount ,
slValidator : this.deps.slValidator ,
} ,
touched ,
) ,
tableExists : ( connectionId , tableRef ) = >
this . tableRefExistsInSemanticLayer (
this . deps . semanticLayerService . forWorktree ( sessionWorktree . workdir ) ,
[ connectionId ] ,
tableRef ,
) ,
} ) ;
} ,
) ;
} catch ( error ) {
const gateError = this . errorMessage ( error ) ;
const repairPaths = finalGateRepairPaths ( {
changedWikiPageKeys : finalChangedWikiPageKeys ,
touchedSlSources : finalTouchedSlSources ,
} ) ;
emitStageProgress ( 'final_gates' , 89 , 'Repairing final artifact gates' ) ;
const gateRepair = await repairFinalGateFailure ( {
agentRunner : this.deps.agentRunner ,
workdir : sessionWorktree.workdir ,
gateError ,
allowedPaths : repairPaths ,
trace : runTrace ,
repairKind : 'final_artifact_gate' ,
maxAttempts : 1 ,
stepBudget : 16 ,
} ) ;
isolatedDiffSummary . gateRepairAttempts += gateRepair . attempts ;
if ( gateRepair . status === 'failed' ) {
isolatedDiffSummary . gateRepairFailures += 1 ;
activeFailureDetails = {
. . . finalArtifactGateTraceData ,
gateRepair ,
gateError ,
} ;
throw new Error ( ` ${ gateError } \ ngate repair failed: ${ gateRepair . reason } ` ) ;
}
isolatedDiffSummary . gateRepairs += 1 ;
await traceTimed (
runTrace ,
'final_gates' ,
'final_artifact_gates_after_gate_repair' ,
{
. . . finalArtifactGateTraceData ,
repairedPaths : gateRepair.changedPaths ,
} ,
async ( ) = > {
await validateFinalIngestArtifacts ( {
connectionIds : repairConnectionIds ,
changedWikiPageKeys : finalChangedWikiPageKeys ,
touchedSlSources : finalTouchedSlSources ,
wikiService : this.deps.wikiService.forWorktree ( sessionWorktree . workdir ) ,
semanticLayerService : this.deps.semanticLayerService.forWorktree ( sessionWorktree . workdir ) ,
validateTouchedSources : ( touched ) = >
validateWuTouchedSources (
{
semanticLayerService : this.deps.semanticLayerService.forWorktree ( sessionWorktree . workdir ) ,
connections : this.deps.connections ,
configService : sessionWorktree.config ,
gitService : sessionWorktree.git ,
slSourcesRepository : this.deps.slSourcesRepository ,
probeRowCount : this.deps.settings.probeRowCount ,
slValidator : this.deps.slValidator ,
} ,
touched ,
) ,
tableExists : ( connectionId , tableRef ) = >
this . tableRefExistsInSemanticLayer (
this . deps . semanticLayerService . forWorktree ( sessionWorktree . workdir ) ,
[ connectionId ] ,
tableRef ,
) ,
} ) ;
} ,
) ;
const repairCommit = await sessionWorktree . git . commitFiles (
gateRepair . changedPaths ,
` ingest( ${ job . sourceKey } ): repair final gates syncId= ${ syncId } ` ,
this . deps . storage . systemGitAuthor . name ,
this . deps . storage . systemGitAuthor . email ,
) ;
if ( ! repairCommit . created ) {
isolatedDiffSummary . gateRepairFailures += 1 ;
throw new Error ( 'final gate repair produced no committable changes' ) ;
}
await runTrace . event ( 'debug' , 'final_gates' , 'final_gate_repair_committed' , {
commitSha : repairCommit.commitHash ,
repairedPaths : gateRepair.changedPaths ,
} ) ;
}
activeFailureDetails = undefined ;
activePhase = 'provenance_validation' ;
emitStageProgress ( 'provenance' , 90 , 'Validating provenance rows' ) ;
latestReportWorkUnits = this . toReportWorkUnits ( stageIndex ) ;
latestReconciliationActions = reconcileActions ;
latestConflictsResolved = stageIndex . conflictsResolved ;
latestEvictionsApplied = stageIndex . evictionsApplied ;
latestUnmappedFallbacks = stageIndex . unmappedFallbacks ;
latestArtifactResolutions = stageIndex . artifactResolutions ? ? [ ] ;
latestEvictionInputs = eviction ? . deletedRawPaths ? ? [ ] ;
latestUnresolvedCards = unresolvedCards ? ? [ ] ;
2026-05-20 14:17:10 +02:00
const finalizationProvenance = this . partitionFinalizationActionsForProvenance ( {
actions : finalizationActions ,
currentRawPaths : new Set ( currentHashes . keys ( ) ) ,
currentEvictionRawPaths : new Set ( stageIndex . evictionsApplied . map ( ( entry ) = > entry . rawPath ) ) ,
overrideEvictionRawPaths : new Set ( overrideReport ? . body . evictionInputs ? ? [ ] ) ,
} ) ;
if ( finalizationOutcome ) {
finalizationOutcome . provenanceExclusions = finalizationProvenance . exclusions ;
latestFinalizationOutcome = finalizationOutcome ;
}
2026-05-18 13:38:06 +02:00
const provenancePlan = this . buildProvenancePlan ( {
job ,
syncId ,
currentHashes ,
stageIndex ,
reconcileActions ,
2026-05-20 14:17:10 +02:00
finalizationActions : finalizationProvenance.actions ,
2026-05-18 13:38:06 +02:00
} ) ;
const provenanceRows = provenancePlan . rows ;
const currentRawPaths = new Set ( currentHashes . keys ( ) ) ;
const deletedRawPaths = new Set ( eviction ? . deletedRawPaths ? ? [ ] ) ;
const provenanceValidationData = this . provenanceValidationTraceData ( {
plan : provenancePlan ,
currentRawPaths ,
deletedRawPaths ,
} ) ;
const reportProvenanceRows = this . toReportProvenanceRows ( provenanceRows ) ;
latestReportProvenanceRows = reportProvenanceRows ;
activeFailureDetails = provenanceValidationData ;
await traceTimed (
runTrace ,
'provenance' ,
'provenance_rows_validation' ,
provenanceValidationData ,
async ( ) = > {
validateProvenanceRawPaths ( {
rows : provenanceRows ,
currentRawPaths ,
deletedRawPaths ,
} ) ;
} ,
) ;
activeFailureDetails = undefined ;
2026-05-13 13:43:23 +02:00
2026-05-10 23:12:26 +02:00
// Stage 6 — squash commit
2026-05-18 13:38:06 +02:00
activePhase = 'squash' ;
2026-05-10 23:12:26 +02:00
const stage6 = ctx ? . startPhase ( 0.04 ) ;
2026-05-18 13:38:06 +02:00
emitStageProgress ( 'save' , 91 , 'Saving changes' ) ;
2026-05-10 23:12:26 +02:00
await stage6 ? . updateProgress ( 0.0 , 'Saving changes' ) ;
try {
await sessionWorktree . git . assertWorktreeClean ( ) ;
} catch ( error ) {
await this . deps . runs . markFailed ( runRow . id ) ;
throw error ;
}
const commitMessage = this . buildCommitMessage ( job , syncId , diffSummary , failedWorkUnits ) ;
const squashResult = await this . deps . lockingService . withLock ( 'config:repo' , async ( ) = > {
const preSquashSha = await this . deps . gitService . revParseHead ( ) ;
const merge = await this . deps . gitService . squashMergeIntoMain (
sessionWorktree . branch ,
this . deps . storage . systemGitAuthor . name ,
this . deps . storage . systemGitAuthor . email ,
commitMessage ,
) ;
return { preSquashSha , merge } ;
} ) ;
const mergeResult = squashResult . merge ;
if ( ! mergeResult . ok ) {
await this . deps . runs . markFailed ( runRow . id ) ;
throw new Error ( ` squash merge conflict: ${ mergeResult . conflictPaths . join ( ', ' ) } ` ) ;
}
const commitSha = mergeResult . touchedPaths . length === 0 ? null : mergeResult . squashSha ;
2026-05-18 13:38:06 +02:00
await runTrace . event ( 'debug' , 'squash' , 'squash_finished' , {
commitSha ,
touchedPaths : mergeResult.touchedPaths ,
} ) ;
2026-05-20 14:17:10 +02:00
const memoryFlowSavedActions = stageIndex . workUnits
. flatMap ( ( wu ) = > wu . actions )
. concat ( reconcileActions )
. concat ( finalizationActions ) ;
2026-05-10 23:12:26 +02:00
memoryFlow ? . emit ( {
type : 'saved' ,
commitSha ,
2026-05-20 14:17:10 +02:00
wikiCount : countMemoryFlowActions ( memoryFlowSavedActions , 'wiki' ) ,
slCount : countMemoryFlowActions ( memoryFlowSavedActions , 'sl' ) ,
2026-05-10 23:12:26 +02:00
} ) ;
await stage6 ? . updateProgress ( 1.0 , commitSha ? ` Saved changes ( ${ commitSha . slice ( 0 , 8 ) } ) ` : 'No changes to save' ) ;
// Sync the shared `knowledge` index from the squashed diff in a single
// transaction. If this throws, the run fails and no partial index state
// survives (thanks to the transactional upsert in applyDiffTransactional).
if ( commitSha ) {
// Multi-file squash → omit path so the handler diffs the whole commit
// (a comma-joined pathspec would match nothing and the job would no-op).
const pathFilter = mergeResult . touchedPaths . length === 1 ? mergeResult . touchedPaths [ 0 ] : '' ;
await this . deps . commitMessages . enqueueForExternalCommit ( { commitHash : commitSha } , commitMessage , pathFilter ) ;
await this . deps . wikiService . syncFromCommit ( squashResult . preSquashSha , commitSha , runRow . id ) ;
await this . syncKnowledgeSlRefsFromActions ( job . connectionId , memoryFlowSavedActions ) ;
const touchedConnections = [
. . . new Set (
memoryFlowSavedActions
. filter ( ( action ) = > action . target === 'sl' )
. map ( ( action ) = > actionTargetConnectionId ( action , job . connectionId ) )
2026-05-20 14:17:10 +02:00
. concat ( finalizationTouchedSources . map ( ( source ) = > source . connectionId ) ) ,
2026-05-10 23:12:26 +02:00
) ,
] . sort ( ) ;
for ( const connectionId of touchedConnections ) {
try {
fix(context): merge overlay columns onto manifest columns by name (#94)
* fix(context): merge overlay columns onto manifest columns by name
composeOverlay was appending overlay columns to the manifest column list,
producing duplicate entries when dbt/metabase overlays declared a column
just to attach descriptions. The duplicates carried no `type`, so the
pydantic SourceDefinition rejected them at semantic-query time and broke
`ktx sl query` for every overlay-backed measure. Now overlay columns
match base columns by name (case-insensitive): same-name entries merge
onto the manifest (overlay fields win, type/role fall back to the base,
descriptions merge per source key) and only new names append.
* refactor(sl): split overlay columns from column_overrides and enforce TS/Python wire contract
Overlay sources now have two distinct collections: `columns:` for computed
columns (requiring `expr` + `type`) and `column_overrides:` for metadata
patches to inherited manifest columns. Composing or loading an overlay that
mixes the two — or references an unknown column — fails with a typed error.
Introduce `ResolvedSemanticLayerSource` / `resolvedSourceSchema` /
`toResolvedWire` as the strict shape sent to the Python engine, and add a
schema contract test that diffs Zod against the Pydantic JSON schema dumped
by `python -m semantic_layer dump-schema`. `SourceDefinition` is now
`extra="forbid"` on the Python side.
`loadAllSources` surfaces per-file load errors instead of swallowing them,
so validation/query paths can report manifest shard parse failures.
* fix(context): make scan description generation resilient and quiet
A transient sampleTable failure during ingest used to take out every
table in a connection: generateTableDescription returned a hardcoded
'Table not found' string into descriptions.ai, and KtxDescriptionGenerator
was constructed without a logger, so the failure left no trail anywhere.
- sampleTable / sampleColumn calls retry 3x with 200/400/800ms backoff,
honouring KtxScanContext.signal via a new KtxAbortedError.
- On retry exhaustion or missing capability, table generation falls back
to a metadata-only prompt built from column name / native type / comment
/ rawDescriptions. The column path follows the same rule -- call the
LLM when any of samples or rawDescriptions are available; skip only
when both are absent.
- Logger is now threaded from KtxScanContext into the generator. Failures
emit structured KtxScanWarning entries (new description_fallback_used
code, plus existing sampling_failed / enrichment_failed /
connector_capability_missing). ktx scan groups warnings by code so a
batch of identical failures collapses to one summary line plus sample.
- Returns null on failure instead of the 'Table not found' sentinel; the
manifest writer's existing guard already skips empty descriptions, so
schema YAML no longer carries misleading text. SCAN_MANAGED_DESCRIPTION_KEYS
already strips stale 'ai' on merge, so existing YAML clears on next run.
Also suppress AI SDK v6 'system in messages' warning: pull system messages
out of KtxMessageBuilder.wrapSimple's output via a new splitKtxSystemMessages
helper and pass them top-level to generateText (preserves cacheControl
providerOptions on the SystemModelMessage). Agent-runner's local
splitSystemPromptMessages dedupes onto the shared helper.
* test(docs): align examples-docs assertions with revamped docs
PR #103 (setup/guide doc revamp) reworded several CLI examples and
connection labels; the assertions in scripts/examples-docs.test.mjs
still referenced the pre-revamp wording and were failing in CI on main.
Update the regexes to match the post-revamp content:
- drop the `--json` flag from the sl-query example expectation
- move the `Driver:` / `Status: ok` probe to the connection reference,
which is where that output now lives (driver id is lowercase
`postgres`, not the display name `PostgreSQL`)
- drop the obsolete `Install \`uv\`...` troubleshooting line
- accept `<connectionId>` everywhere; the docs no longer use the
hyphenated `<connection-id>` form
- match the `warehouse` connection id used in the quickstart instead of
the `postgres-warehouse` id only used in the README and setup ref
* fix(sl): skip TS/Python schema contract test when uv is unavailable
The TypeScript checks CI job does not install uv or Python, so the
module-level `execFileSync('uv', ...)` in schemas.contract.test.ts threw
ENOENT and failed the suite. Wrap the schema dump in a try/catch and
guard the describe block with `describe.skipIf` so the test skips in
environments without uv. Local dev and any CI job that has uv on PATH
still runs the cross-language contract assertion.
2026-05-15 02:11:04 +02:00
const { sources : allSources } = await this . deps . semanticLayerService . loadAllSources ( connectionId ) ;
2026-05-10 23:12:26 +02:00
await this . deps . slSearchService . indexSources ( connectionId , allSources ) ;
} catch ( err ) {
this . logger . warn (
` [ingest-bundle] post-squash SL reindex failed for connection= ${ connectionId } : ${ err instanceof Error ? err.message : String ( err ) } ` ,
) ;
}
}
}
const stage5 = ctx ? . startPhase ( 0.04 ) ;
2026-05-18 13:38:06 +02:00
emitStageProgress ( 'provenance' , 95 , 'Recording history' ) ;
2026-05-10 23:12:26 +02:00
await stage5 ? . updateProgress ( 0.0 , 'Recording history' ) ;
2026-05-18 13:38:06 +02:00
activePhase = 'provenance' ;
2026-05-10 23:12:26 +02:00
await this . deps . provenance . insertMany ( provenanceRows ) ;
2026-05-18 13:38:06 +02:00
await runTrace . event ( 'debug' , 'provenance' , 'provenance_rows_inserted' , {
rowCount : provenanceRows.length ,
} ) ;
2026-05-10 23:12:26 +02:00
memoryFlow ? . emit ( { type : 'provenance_recorded' , rowCount : provenanceRows.length } ) ;
await stage5 ? . updateProgress (
1.0 ,
` Recorded ${ provenanceRows . length } history entr ${ provenanceRows . length === 1 ? 'y' : 'ies' } ` ,
) ;
const stage7 = ctx ? . startPhase ( 0.04 ) ;
2026-05-18 13:38:06 +02:00
emitStageProgress ( 'report' , 97 , 'Wrapping up' ) ;
2026-05-10 23:12:26 +02:00
await stage7 ? . updateProgress ( 0.0 , 'Wrapping up' ) ;
2026-05-18 13:38:06 +02:00
activePhase = 'report' ;
2026-05-10 23:12:26 +02:00
const reportToolTranscripts = Array . from ( transcriptSummaries . values ( ) ) . map ( ( summary ) = > ( {
unitKey : summary.unitKey ,
path : summary.path ,
toolCallCount : summary.toolCallCount ,
errorCount : summary.errorCount ,
toolNames : Array.from ( summary . toolNames ) . sort ( ) ,
} ) ) ;
const capturedMemoryFlow = memoryFlow ? . snapshot ( ) ;
const reportMemoryFlow = capturedMemoryFlow
? {
. . . capturedMemoryFlow ,
metadata : {
schemaVersion : 1 as const ,
mode : 'full' as const ,
origin : 'captured' as const ,
timing : 'captured' as const ,
capturedAt : new Date ( ) . toISOString ( ) ,
sourceReportId : null ,
sourceReportPath : null ,
fallbackReason : null ,
} ,
}
: undefined ;
const reportBody = {
2026-05-18 13:38:06 +02:00
status : 'completed' as const ,
2026-05-10 23:12:26 +02:00
syncId ,
diffSummary ,
fetch : fetchReport ? ? undefined ,
commitSha ,
2026-05-18 13:38:06 +02:00
tracePath : runTrace.tracePath ,
isolatedDiff : ! overrideReport ? isolatedDiffSummary : undefined ,
2026-05-10 23:12:26 +02:00
workUnits : stageIndex.workUnits.map ( ( wu ) = > ( {
unitKey : wu.unitKey ,
rawFiles : wu.rawFiles ,
status : wu.status ,
reason : wu.reason ,
actions : wu.actions ,
touchedSlSources : wu.touchedSlSources ,
slDisallowed : wu.slDisallowed ,
slDisallowedReason : wu.slDisallowedReason ,
} ) ) ,
failedWorkUnits ,
reconciliationSkipped : reconcileOutcome.skipped ,
conflictsResolved : stageIndex.conflictsResolved ,
evictionsApplied : stageIndex.evictionsApplied ,
unmappedFallbacks : stageIndex.unmappedFallbacks ,
artifactResolutions : stageIndex.artifactResolutions ? ? [ ] ,
evictionInputs : eviction?.deletedRawPaths ? ? [ ] ,
reconciliationActions : reconcileActions ,
evictionDecisions : rcToolSession.evictionDecisions ? ? [ ] ,
unresolvedCards : unresolvedCards ? ? [ ] ,
supersededBy : null ,
overrideOf : overrideReport?.jobId ? ? null ,
provenanceRows : reportProvenanceRows ,
toolTranscripts : reportToolTranscripts ,
2026-05-20 14:17:10 +02:00
finalization : finalizationOutcome ,
2026-05-13 13:43:23 +02:00
wikiSlRefRepairs : wikiSlRefRepairResult.repairs ,
wikiSlRefRepairWarnings : wikiSlRefRepairResult.warnings ,
2026-05-10 23:12:26 +02:00
. . . ( reportMemoryFlow ? { memoryFlow : reportMemoryFlow } : { } ) ,
context : contextReport
? {
documentsIndexed : contextReport.documentsIndexed ,
chunksIndexed : contextReport.chunksIndexed ,
documentsDeleted : contextReport.documentsDeleted ,
embeddingFailures : contextReport.embeddingFailures ,
candidatesCreated : candidateSummaryAfterReconcile?.total ? ? 0 ,
candidatesPromoted : candidateSummaryAfterReconcile?.promoted ? ? 0 ,
candidatesRejected : candidateSummaryAfterReconcile?.rejected ? ? 0 ,
triage : triageResult?.report ,
dedup : dedupResult?.enabled
? {
candidatesIn : dedupResult.candidatesIn ,
clustersOut : dedupResult.clustersOut ,
mergedCount : dedupResult.mergedCount ,
largestClusterSize : dedupResult.largestClusterSize ,
embeddingFailures : dedupResult.embeddingFailures ,
}
: undefined ,
curator : curatorReport ? ? undefined ,
knowledgeCreates : stageIndex.workUnits
. flatMap ( ( wu ) = > wu . actions )
. concat ( reconcileActions )
. filter ( ( action ) = > action . target === 'wiki' && action . type === 'created' ) . length ,
knowledgeUpdates : stageIndex.workUnits
. flatMap ( ( wu ) = > wu . actions )
. concat ( reconcileActions )
. filter ( ( action ) = > action . target === 'wiki' && action . type === 'updated' ) . length ,
capped : sourceContextReport?.capped ? ? false ,
warnings : [
. . . new Set ( [
. . . contextReport . warnings ,
. . . ( sourceContextReport ? . warnings ? ? [ ] ) ,
. . . ( triageResult ? . warnings ? ? [ ] ) ,
. . . ( carryForwardResult ? . warnings ? ? [ ] ) ,
. . . ( dedupResult ? . warnings ? ? [ ] ) ,
. . . curatorWarnings ,
] ) ,
] ,
}
: undefined ,
} ;
const createdReport = await this . deps . reports . create ( {
runId : runRow.id ,
jobId : job.jobId ,
connectionId : job.connectionId ,
sourceKey : job.sourceKey ,
body : reportBody ,
} ) ;
const reportId = reportIdFromCreateResult ( createdReport ) ;
2026-05-18 13:38:06 +02:00
await runTrace . event ( 'debug' , 'report' , 'success_report_created' , {
reportId ,
runId : runRow.id ,
tracePath : runTrace.tracePath ,
} ) ;
2026-05-10 23:12:26 +02:00
memoryFlow ? . update ( {
. . . ( reportId ? { reportId , reportPath : reportId } : { } ) ,
} ) ;
memoryFlow ? . emit ( {
type : 'report_created' ,
runId : runRow.id ,
. . . ( reportId ? { reportPath : reportId } : { } ) ,
} ) ;
if ( overrideReport ) {
await this . deps . reports . markSuperseded ( overrideReport . jobId , job . jobId ) ;
}
if ( contextReport && this . deps . contextEvidenceIndex ) {
await this . deps . contextEvidenceIndex . publishSync ( {
connectionId : job.connectionId ,
sourceKey : job.sourceKey ,
syncId ,
diffSet ,
} ) ;
}
// Stage 7 — status
await this . deps . runs . markCompleted (
runRow . id ,
diffSummary ,
fetchReport ? . status === 'partial' ? 'partial' : 'completed' ,
) ;
if ( job . bundleRef . kind === 'scheduled_pull' ) {
await adapter . onPullSucceeded ? . ( {
connectionId : job.connectionId ,
sourceKey : job.sourceKey ,
syncId ,
trigger : job.trigger ,
completedAt : new Date ( ) ,
stagedDir ,
} ) ;
}
await stage7 ? . updateProgress ( 1.0 , 'Done' ) ;
2026-05-18 13:38:06 +02:00
await runTrace . event ( 'info' , 'run' , 'ingest_finished' , {
status : 'completed' ,
commitSha ,
failedWorkUnits ,
tracePath : runTrace.tracePath ,
} ) ;
2026-05-10 23:12:26 +02:00
cleanupOutcome = 'success' ;
return {
runId : runRow.id ,
syncId ,
diffSummary ,
workUnitCount : workUnits.length ,
failedWorkUnits ,
artifactsWritten : provenanceRows.filter ( ( r ) = > r . actionType !== 'skipped' ) . length ,
commitSha ,
} ;
} finally {
await this . deps . sessionWorktreeService . cleanup ( sessionWorktree , cleanupOutcome ) ;
}
2026-05-18 13:38:06 +02:00
} catch ( error ) {
await activeTrace . event (
'error' ,
'run' ,
'ingest_failed' ,
{
tracePath : activeTrace.tracePath ,
phase : activePhase ,
runId : runRow?.id ? ? null ,
syncId ,
} ,
error ,
) ;
if ( runRow ) {
await this . deps . runs . markFailed ( runRow . id ) ;
await this . deps . reports . create ( {
runId : runRow.id ,
jobId : job.jobId ,
connectionId : job.connectionId ,
sourceKey : job.sourceKey ,
body : {
status : 'failed' as const ,
syncId ,
diffSummary : latestDiffSummary ,
commitSha : null ,
tracePath : activeTrace.tracePath ,
isolatedDiff : latestIsolatedDiffSummary ,
failure : {
phase : activePhase ,
message : this.errorMessage ( error ) ,
. . . ( activeFailureDetails ? { details : activeFailureDetails } : { } ) ,
} ,
workUnits :
latestReportWorkUnits . length > 0
? latestReportWorkUnits
: latestWorkUnits . map ( ( wu ) = > ( {
unitKey : wu.unitKey ,
rawFiles : [ ] ,
status : wu.status ,
reason : wu.reason ,
actions : wu.actions ,
touchedSlSources : wu.touchedSlSources ,
slDisallowed : wu.slDisallowed ,
slDisallowedReason : wu.slDisallowedReason ,
} ) ) ,
failedWorkUnits : latestFailedWorkUnits ,
reconciliationSkipped : latestReconciliationSkipped ,
conflictsResolved : latestConflictsResolved ,
evictionsApplied : latestEvictionsApplied ,
unmappedFallbacks : latestUnmappedFallbacks ,
artifactResolutions : latestArtifactResolutions ,
evictionInputs : latestEvictionInputs ,
reconciliationActions : latestReconciliationActions ,
2026-05-20 14:17:10 +02:00
finalization : latestFinalizationOutcome ,
2026-05-18 13:38:06 +02:00
evictionDecisions : [ ] ,
unresolvedCards : latestUnresolvedCards ,
supersededBy : null ,
overrideOf : null ,
provenanceRows : latestReportProvenanceRows ,
toolTranscripts : Array.from ( transcriptSummaries . values ( ) ) . map ( ( summary ) = > ( {
unitKey : summary.unitKey ,
path : summary.path ,
toolCallCount : summary.toolCallCount ,
errorCount : summary.errorCount ,
toolNames : Array.from ( summary . toolNames ) . sort ( ) ,
} ) ) ,
} ,
} ) ;
await activeTrace . event ( 'info' , 'report' , 'failure_report_created' , {
runId : runRow.id ,
jobId : job.jobId ,
tracePath : activeTrace.tracePath ,
} ) ;
}
throw error ;
}
2026-05-10 23:12:26 +02:00
}
}